Docker Swarm Constraint Service (Microservices)
Tôi đã viết ứng dụng Python "thực" đầu tiên của mình. Tôi chưa bao giờ làm việc nhiều với Python trước đây, vì vậy tôi muốn nhận được phản hồi về cách tôi có thể cấu trúc ứng dụng để làm cho nó theo cách các chương trình Python thường làm.
Điều này rất quan trọng từ góc độ "dễ đọc", nhưng nhận được phản hồi về cấu trúc chung cũng như việc tôi sử dụng các lớp, đặt tên cho các phương thức và sử dụng các bình luận sẽ rất tốt để lấy ý kiến của người khác.
https://github.com/sbrattla/swarmconstraint
Trường hợp sử dụng cho ứng dụng này là có thật. Tôi quản lý một Docker Swarm, nhưng tôi cần một dịch vụ (còn gọi là nhiệm vụ trong Docker Swarm) để chỉ chạy trong một vài nút tham gia vào bầy đó. Tuy nhiên, nếu tôi sử dụng các ràng buộc về vị trí (ràng buộc một dịch vụ với các nút cụ thể) và các nút mà dịch vụ bị ràng buộc đi xuống - thì dịch vụ cũng sẽ đi xuống. Vì vậy, ứng dụng sẽ loại bỏ các ràng buộc về vị trí nếu các nút được chỉ định bị hỏng để dịch vụ có thể "dự phòng" cho các nút khác.
#!/usr/bin/python3
import argparse
import docker
import json
import logging
import re
import string
import time
class SwarmConstraint:
def __init__(self, args):
self.args = args
self.initClient()
self.logger = logging.getLogger(__name__)
handler = logging.StreamHandler()
formatter = logging.Formatter(
'%(asctime)-25s %(levelname)-8s %(message)s')
handler.setFormatter(formatter)
self.logger.addHandler(handler)
self.logger.setLevel(logging.DEBUG)
if (not self.args['watch']):
raise Exception('At least one node to watch must be provided.')
if (not self.args['toggle']):
raise Exception('At least one node to toggle must be provided.')
if (not self.args['label']):
raise Exception('At least one label must be provided.')
if (not self.args['prefix']):
raise Exception('A prefix must be provided.')
self.logger.info('Watch {watch}.'.format(watch=','.join(self.args['watch'])))
self.logger.info('Toggle the label(s) {labels} on {toggle}.'.format(labels=','.join(self.args['label']), toggle=','.join(self.args['toggle'])))
self.logger.info('Prefix disabled labels with {prefix}.'.format(prefix=self.args['prefix']))
def run(self):
# Collect availability for watched nodes, and keep track of the collective
# availability for all the watched nodes.
nodes = self.getNodes()
allWatchedNodesUnavailable = True
for nodeId in nodes:
watchNode = nodes[nodeId]
if (not self.args['watch'] or watchNode['hostname'] not in self.args['watch']):
continue
if (self.isNodeAvailable(watchNode) == True):
allWatchedNodesUnavailable = False
break;
if (allWatchedNodesUnavailable):
self.logger.warn('All watched nodes are unavailable.')
else:
self.logger.debug('One or more watched nodes are available.')
# Disable or enable labels depending on the collective availability for all
# the watched nodes.
for nodeId in nodes:
toggleNode = nodes[nodeId]
if (self.args['toggle'] and toggleNode['hostname'] not in self.args['toggle']):
continue
if (allWatchedNodesUnavailable):
self.disableLabels(toggleNode, self.args['label'], self.args['prefix'])
else:
self.enableLabels(toggleNode, self.args['label'], self.args['prefix'])
def getSocket(self):
return 'unix://var/run/docker.sock'
def initClient(self):
# Initialize the docker client.
socket = self.getSocket()
self.client = docker.DockerClient(base_url=socket)
def getNodes(self):
# Returns all nodes.
allNodes = self.client.nodes.list();
allNodesMap = {}
for node in allNodes:
allNodesMap[node.id] = {
'id' : node.id,
'available' : True if node.attrs['Spec']['Availability'] == 'active' else False,
'hostname': node.attrs['Description']['Hostname'],
'role' : node.attrs['Spec']['Role'],
'platform' : {
'os' : node.attrs['Description']['Platform']['OS'],
'arch' : node.attrs['Description']['Platform']['Architecture']
},
'labels' : node.attrs['Spec']['Labels'],
}
return allNodesMap
def isNodeAvailable(self, node):
return node['available']
def disableLabels(self, node, labels, prefix):
# Disable labels on a node by adding a prefix to each label. The node will only be
# updated if at least one of the provided labels are currently enabled.
matchingNode = next(iter(self.client.nodes.list(filters={'id':node['id']})), None)
if (matchingNode is None):
return
spec = matchingNode.attrs['Spec']
update = False
for label in labels:
if (label not in spec['Labels']):
continue
nodeLabelKey = label
nodeLabelVal = spec['Labels'][nodeLabelKey]
spec['Labels'].update(self.prefixNodeLabel(nodeLabelKey, nodeLabelVal, prefix))
spec['Labels'].pop(nodeLabelKey, None)
update = True
self.logger.info('Disabling the label "{key}={val} on {node}".'.format(key=nodeLabelKey, val=nodeLabelVal, node=node['id']))
if (update):
matchingNode.update(spec)
return True
else:
return False
def enableLabels(self, node, labels, prefix):
# Enable labels on a node by removing the prefix from each label. The node will only be
# updated if at least one of the provided labels are currently disabled.
matchingNode = next(iter(self.client.nodes.list(filters={'id':node['id']})), None)
if (matchingNode is None):
return
spec = matchingNode.attrs['Spec']
update = False
for label in labels:
label = self.prefixLabel(label, prefix)
if (label not in spec['Labels']):
continue
nodeLabelKey = label
nodeLabelVal = spec['Labels'][nodeLabelKey]
spec['Labels'].update(self.unPrefixNodeLabel(nodeLabelKey, nodeLabelVal, prefix))
spec['Labels'].pop(nodeLabelKey, None)
update = True
self.logger.info('Enabling the label "{key}={val} on {node}".'.format(key=nodeLabelKey, val=nodeLabelVal, node=node['id']))
if (update):
matchingNode.update(spec)
return True
else:
return False
def prefixLabel(self, label, prefix):
# Split and prefix a label into a dictionary holding the prefixed key and the value separately.
return '{prefix}.{key}'.format(prefix=prefix, key=label)
def isNodeLabelPrefixed(self, key, prefix):
# Evaluates if a node label is prefixed
return True if key.find(prefix) > -1 else False;
def prefixNodeLabel(self, key, val, prefix):
# Prefix a node label.
label = {'{prefix}.{key}'.format(prefix=prefix,key=key) : '{val}'.format(val=val)}
return label
def unPrefixNodeLabel(self, key, val, prefix):
# Remove prefix from a node label.
key = key.replace('{prefix}.'.format(prefix=prefix), '')
label = {'{key}'.format(prefix=prefix,key=key) : '{val}'.format(val=val)}
return label
class FromFileAction(argparse.Action):
def __init__(self, option_strings, dest, nargs=None, **kwargs):
super(FromFileAction, self).__init__(option_strings, dest, **kwargs)
def __call__(self, parser, namespace, path, option_string=None):
if (path):
data = None
with open(path) as f:
data = json.load(f)
if (data is None):
return
if ('watch' in data):
namespace.watch += data['watch']
if (data['toggle']):
namespace.toggle += data['toggle']
if ('label' in data):
namespace.label += data['label']
return
def main():
parser = argparse.ArgumentParser(description='Toggles one or more constraints depending on node availability')
parser.add_argument('--watch', metavar='watch', action='append', default=[], help='A node which availability is to be watched.')
parser.add_argument('--toggle', metavar='toggle', action='append', default=[], help='A node for which constraints are to be toggled. Defaults to all nodes.')
parser.add_argument('--label', metavar='label', action='append', default=[], help='A label which is to be toggled according to availability for watched nodes.')
parser.add_argument('--prefix', metavar='prefix', default='disabled', help='The prefix to use for disabled labels. Defaults to "disabled".')
parser.add_argument('fromFile', action=FromFileAction, help='A file which holds configurations.')
args = vars(parser.parse_args())
se = SwarmConstraint(args)
while(True):
try:
se.run()
time.sleep(10)
except KeyboardInterrupt:
break
except Exception as err:
print(err)
break
if __name__ == '__main__':
main()
Trả lời
Chào mừng bạn đến với Đánh giá mã!
PEP-8
Trong python, thông thường (và được khuyến nghị) làm theo hướng dẫn kiểu PEP-8 để viết mã sạch, có thể bảo trì và nhất quán.
Các hàm và biến phải được đặt tên trong a lower_snake_case, các lớp là UpperCamelCasevà các hằng là UPPER_SNAKE_CASE.
Bạn không cần chỉ định dấu ngoặc quanh các kiểm tra điều kiện trong if-/ elif-mệnh đề.
Thụt lề cho chương trình phải tuân theo 4 khoảng trắng cho mỗi kiểu cấp độ thụt lề.
f-string trên str.format
Trong số 2 cái sau, bạn thích cái nào hơn? (cả hai đều làm giống nhau)
f"{var} value""{var} value".format(var=var)
Cái trước được gọi là f-string và mới được giới thiệu trong python 3.
argparse
Bạn đang thu thập tất cả các đối số từ dòng lệnh bằng cách sử dụng argparsegói, nhưng thay vì để cho argparse đặt chúng là required, bạn đang xác thực trong quá trình khởi tạo lớp.
Chuỗi ma thuật
Địa chỉ unix socket dường như là một hằng số và không thực sự cần được liên kết với phương thức lớp. Trừ khi bạn lập kế hoạch cho điều này cũng được cung cấp từ cli, hãy để nó được định nghĩa là một giá trị không đổi. Tương tự với trình định dạng ghi nhật ký, v.v.
Phức tạp vô dụng
Bạn tập hợp tất cả các nút từ ứng dụng khách docker swarm vào một biến, mục đích duy nhất là để được lặp lại, không có tham chiếu nào thêm về nó. Lặp lại mà không cần lưu trữ một biến riêng biệt.
Việc tạo từ điển của đối tượng nút có thể được tách thành phương thức riêng của nó, phương thức này đã nhận một nút và trả về từ điển bạn cần.
Tập hợp nút được định nghĩa là get_nodes(quy ước đặt tên chính xác), và đối với disable_labels/ enable_labelsetc, bạn vẫn tổng hợp các nút bằng trình tổng hợp nút của riêng nó.
Ví dụ, trình tổng hợp nút có thể được thực hiện như:
@staticmethod
def process_node(node):
spec = node.attrs["Spec"]
description = node.attrs["Description"]
return {
"id": node.id,
"available": spec["Availability"] == "active",
"hostname": description["Hostname"],
"role": spec["Role"],
"platform": {
"os": description["Platform"]["OS"],
"arch": description["Platform"]["Architecture"],
},
"labels": spec["Labels"],
}
def get_nodes(self):
return {
node.id: self.process_node(node)
for node in self.client.nodes.list()
}
Lưu ý trong node["available"]thuộc tính trên. Bạn không cần câu lệnh if-else ở đó.
Bình luận
Các ý kiến như chúng là hoàn toàn vô ích . Nếu bạn có một cái gì đó như:
def init_client(self):
# Initialize the docker client.
thì nhận xét không phục vụ mục đích thực sự, cũng như tên init_clienttự giải thích. Nếu bạn làm muốn thêm ý kiến, có lẽ chỉ định cách chức năng làm những việc (mặc dù mã nên được làm điều đó).
Mã trùng lặp
Cả hai chức năng enable_labelsvà disable_labelscó phần lớn mã phổ biến trong chúng. Điều này sẽ được giải nén thành chức năng riêng của nó.