New FAMILUG

The PyMiers

Friday, 31 October 2025

Ansible role luôn chạy trước task

Ansible là công cụ tự động hóa "đơn giản", "dễ dùng". Trong một ansible play, có thể dùng tasks, hoặc roles, hoặc cả hai. Thứ tự chạy của chúng có chút bất ngờ.

Thứ tự role và task khi trong cùng play

Playbook order.yml

- hosts: all
  tasks:
    - name: task1
      debug:
        msg: "This is task1"
  roles:
    - role: pika

Role pika: roles/pika/tasks/main.yml:

- name: task in role
  debug:
    msg: this is task in a role

Output:

$ uvx --from 'ansible-core>2.19' ansible-playbook -K -i localhost, order.yml 
BECOME password: 

PLAY [all] **************************************************************************
...
TASK [pika : task in role] **********************************************************
ok: [localhost] => {
    "msg": "this is task in a role"
}

TASK [task1] ************************************************************************
ok: [localhost] => {
    "msg": "This is task1"
}

PLAY RECAP **************************************************************************
localhost                  : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

Thấy role chạy trước task, mặc dù trong order.yml tasks viết trước roles. Lý do bởi thứ tự này được quy định trong tài liệu:

When you use the roles option at the play level, Ansible treats the roles as static imports and processes them during playbook parsing. Ansible executes each play in this order: ... - Each role listed in roles:, in the order listed. Any role dependencies defined in the role's meta/main.yml run first, subject to tag filtering and conditionals. See :ref:role_dependencies for more details. - Any tasks defined in the play.

Một giải pháp để tasks chạy trước role là include role vào 1 task tiếp theo:

- hosts: all
  tasks:
    - name: task1
      debug:
        msg: "This is task1"
    - name: task run role
      include_role:
        name: pika

Output:

TASK [task1] ************************************************************************
ok: [localhost] => {
    "msg": "This is task1"
}

TASK [task run role] ****************************************************************
included: pika for localhost

TASK [pika : task in role] **********************************************************
ok: [localhost] => {
    "msg": "this is task in a role"
}

Kết luận

Bình thường Ansible chạy từ trên xuống dưới, ngoại trừ các trường hợp ngoại lệ, như khi tasks gặp roles.

Hết.

HVN at https://pymi.vn and https://www.familug.org.

Ủng hộ tác giả 🍺

Sunday, 26 October 2025

[Python] Tính toán IP trong mạng

Python từ 3.3 có sẵn thư viện ipaddress rất tiện lợi để tính toán IP trong mạng.

Thư viện ipaddress

$ podman run -it python:3.10-alpine
...
Python 3.10.19 (main, Oct  9 2025, 22:43:20) [GCC 14.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import ipaddress
>>> print(ipaddress.__doc__)
A fast, lightweight IPv4/IPv6 manipulation library in Python.

This library is used to create/poke/manipulate IPv4 and IPv6 addresses
and networks.


>>> ipaddress.
ipaddress.AddressValueError(        ipaddress.IPv4Network(              ipaddress.collapse_addresses(       ipaddress.ip_network(
ipaddress.IPV4LENGTH                ipaddress.IPv6Address(              ipaddress.functools                 ipaddress.summarize_address_range(
ipaddress.IPV6LENGTH                ipaddress.IPv6Interface(            ipaddress.get_mixed_type_key(       ipaddress.v4_int_to_packed(
ipaddress.IPv4Address(              ipaddress.IPv6Network(              ipaddress.ip_address(               ipaddress.v6_int_to_packed(
ipaddress.IPv4Interface(            ipaddress.NetmaskValueError(        ipaddress.ip_interface(
>>> print(ipaddress.IPv4Network.__doc__)
This class represents and manipulates 32-bit IPv4 network + addresses..

    Attributes: [examples for IPv4Network('192.0.2.0/27')]
        .network_address: IPv4Address('192.0.2.0')
        .hostmask: IPv4Address('0.0.0.31')
        .broadcast_address: IPv4Address('192.0.2.32')
        .netmask: IPv4Address('255.255.255.224')
        .prefixlen: 27

Thử tạo 1 mạng với địa chỉ 10.10.1.241/29:

>>> ipaddress.ip_network("10.10.1.241/29", strict=False)
IPv4Network('10.10.1.240/29')

set strict=False vì địa chỉ đầu vào vốn không phải địa chỉ mạng hợp lệ, Python sẽ tự tính giá trị chính xác là 10.10.1.240. Còn 10.10.1.241 là 1 host trong mạng này.

>>> ip = ipaddress.ip_network("10.10.1.241/29", strict=False)
>>> type(ip)
<class 'ipaddress.IPv4Network'>
>>> ip
IPv4Network('10.10.1.240/29')
>>> ip.
ip.address_exclude(   ip.hostmask           ip.is_multicast       ip.netmask            ip.reverse_pointer    ip.version
ip.broadcast_address  ip.hosts()            ip.is_private         ip.network_address    ip.subnet_of(         ip.with_hostmask
ip.compare_networks(  ip.is_global          ip.is_reserved        ip.num_addresses      ip.subnets(           ip.with_netmask
ip.compressed         ip.is_link_local      ip.is_unspecified     ip.overlaps(          ip.supernet(          ip.with_prefixlen
ip.exploded           ip.is_loopback        ip.max_prefixlen      ip.prefixlen          ip.supernet_of(
>>> ip.network_address
IPv4Address('10.10.1.240')
>>> ip.broadcast_address
IPv4Address('10.10.1.247')
>>> ip.netmask
IPv4Address('255.255.255.248')
>>> ip.num_addresses
8
>>> ip.prefixlen
29

IPv4Network object có các attribute:

  • Số IP trong mạng: 8 == 2**(32-29) == 2**3
  • địa chỉ đầu tiên là địa chỉ mạng: 10.10.1.240
  • địa chỉ cuối là địa chỉ broadcast: 10.10.1.247
  • các địa chỉ còn lại từ 241 tới 246 có thể cấp cho các host.

Kiểm tra IP có nằm trong mạng không

>>> ipaddress.IPv4Address("10.10.1.242") in ip
True
>>> ipaddress.IPv4Address("10.10.1.249") in ip
False

Tính tất cả các mạng con /29 trong 10.10.1.0/24

>>> nip = ipaddress.ip_network("10.10.1.0/24")
>>> for n in nip.subnets(new_prefix=29):
...     print(n)
...
10.10.1.0/29
10.10.1.8/29
10.10.1.16/29
10.10.1.24/29
10.10.1.32/29
10.10.1.40/29
10.10.1.48/29
10.10.1.56/29
10.10.1.64/29
10.10.1.72/29
10.10.1.80/29
10.10.1.88/29
10.10.1.96/29
10.10.1.104/29
10.10.1.112/29
10.10.1.120/29
10.10.1.128/29
10.10.1.136/29
10.10.1.144/29
10.10.1.152/29
10.10.1.160/29
10.10.1.168/29
10.10.1.176/29
10.10.1.184/29
10.10.1.192/29
10.10.1.200/29
10.10.1.208/29
10.10.1.216/29
10.10.1.224/29
10.10.1.232/29
10.10.1.240/29
10.10.1.248/29

Kết luận

ipaddress có sẵn trong stdlib và rất tiện lợi.

Hết.

HVN at https://pymi.vn and https://www.familug.org.

Ủng hộ tác giả 🍺

Saturday, 4 October 2025

[TIL] requests đoán "có giáo dục" encoding và có thể sai

requests là thư viện HTTP client phổ biến nhất của Python, nó phổ biến vì dễ dùng, chỉ cần import requests; requests.get là xong. Trông dễ như vậy bởi requests đã cấu hình mặc định hết rất nhiều thứ như: connection, headers, adapter, session, encoding, ... mà mặc định thì nhiều khi đúng, đôi khi sai.

Encoding

Vì Python mặc định encoding trên Linux là utf-8, người dùng dễ mặc định là requests cũng vậy, nhưng thực tế thì:

Tài liệu viết:

When you make a request, Requests makes educated guesses about the encoding of the response based on the HTTP headers. The text encoding guessed by Requests is used when you access r.text. You can find out what encoding Requests is using, and change it, using the r.encoding property:

r.encoding 'utf-8'

r.encoding = 'ISO-8859-1'

If you change the encoding, Requests will use the new value of r.encoding whenever you call r.text. You might want to do this in any situation where you can apply special logic to work out what the encoding of the content will be. For example, HTML and XML have the ability to specify their encoding in their body. In situations like this, you should use r.content to find the encoding, and then set r.encoding. This will let you use r.text with the correct encoding.

requests thực hiện "đoán một cách có học" (educated guesses) encoding của response dựa trên HTTP headers https://github.com/psf/requests/blob/v2.32.5/src/requests/adapters.py#L355.

    response.encoding = get_encoding_from_headers(response.headers)

Xem code thấy nó chỉ dựa trên header content-type:

def get_encoding_from_headers(headers):
    """Returns encodings from given HTTP Header Dict.

    :param headers: dictionary to extract encoding from.
    :rtype: str
    """

    content_type = headers.get("content-type")

    if not content_type:
        return None

    content_type, params = _parse_content_type_header(content_type)

    if "charset" in params:
        return params["charset"].strip("'\"")

    if "text" in content_type:
        return "ISO-8859-1"

    if "application/json" in content_type:
        # Assume UTF-8 based on RFC 4627: https://www.ietf.org/rfc/rfc4627.txt since the charset was unset
        return "utf-8"
# https://github.com/psf/requests/blob/v2.32.5/src/requests/utils.py#L529-L551

nếu có charset trong content-type requests sẽ dùng giá trị của charset. Ví dụ:

Content-Type: text/html; charset=utf-8

Khi không có charset, nếu content-type chứa text dùng ISO-8859-1, còn nếu là json dùng utf-8. Vậy khi dùng với các JSON API, requests sẽ dùng utf-8 nên kết quả luôn như mong đợi, nhưng nếu lấy trang HTML, khi header không set, encoding có thể bị sai.

resp.content vs resp.text

contenttext là 2 property của response object, content chứa byte và người dùng có thể tự decode với encoding tùy ý. text đọc .encoding rồi decode:

In [48]: url = 'https://podcasts.apple.com/jp/podcast/%E3%81%AA%E3%81%8C%E3%82%89%E6%97%A5%E7%B5%8C/id1627014612'
In [49]: resp = requests.get(url)
In [50]: print(resp.encoding)
ISO-8859-1
In [51]: print(resp.headers.get('Content-Type'))
text/html

do header Content-Type không có charset, lại chứa text, nên requests set encoding = 'ISO-8859-1'

In [54]: resp.text.split("<title>")[1].split("</title>")[0]
Out[54]: 'ã\x81ªã\x81\x8cã\x82\x89æ\x97¥çµ\x8c - ã\x83\x9dã\x83\x83ã\x83\x89ã\x82\xadã\x83£ã\x82¹ã\x83\x88 - Apple Podcast'

In [55]: resp.content.split(b"<title>")[1].split(b"</title>")[0].decode("utf-8")
Out[55]: 'ながら日経 - ポッドキャスト - Apple Podcast'

tự set encoding = 'utf-8' để text hiển thị đúng:

In [56]: resp.encoding = 'utf-8'

In [57]: resp.text.split("<title>")[1].split("</title>")[0]
Out[57]: 'ながら日経 - ポッドキャスト - Apple Podcast'

In [62]: resp.connection
Out[62]: <requests.adapters.HTTPAdapter at 0x772dba31ccd0>

PS: curl cũng encode đúng với utf-8:

$ curl -s https://podcasts.apple.com/jp/podcast/%E3%81%AA%E3%81%8C%E3%82%89%E6%97%A5%E7%B5%8C/id1627014612 | grep -o '<title>.*</title>'
<title>ながら日経 - ポッドキャスト - Apple Podcast</title>

Xem code method text:

    @property
    def text(self):
        """Content of the response, in unicode.

        If Response.encoding is None, encoding will be guessed using
        ``charset_normalizer`` or ``chardet``.

        The encoding of the response content is determined based solely on HTTP
        headers, following RFC 2616 to the letter. If you can take advantage of
        non-HTTP knowledge to make a better guess at the encoding, you should
        set ``r.encoding`` appropriately before accessing this property.
        """

        # Try charset from content-type
        content = None
        encoding = self.encoding

        if not self.content:
            return ""

        # Fallback to auto-detected encoding.
        if self.encoding is None:
            encoding = self.apparent_encoding

        # Decode unicode from given encoding.
        try:
            content = str(self.content, encoding, errors="replace")
        except (LookupError, TypeError):
            # A LookupError is raised if the encoding was not found which could
            # indicate a misspelling or similar mistake.
            #
            # A TypeError can be raised if encoding is None
            #
            # So we try blindly encoding.
            content = str(self.content, errors="replace")

        return content

https://github.com/psf/requests/blob/v2.32.5/src/requests/models.py#L909

Kết luận

requests dễ nhưng không đơn giản.

Hết.

HVN at https://pymi.vn and https://www.familug.org.

Ủng hộ tác giả 🍺