AndyWalshe | c691f02 | 2019-06-10 23:20:41 +0000 | [diff] [blame] | 1 | #!/usr/bin/env python |
| 2 | """ |
| 3 | This script attempts to probe an ONAP deployment in a Kubernetes cluster on an OpenStack stack |
| 4 | and extract all pods and corresponding docker image versions along with some of the tool versions utilised. |
| 5 | """ |
| 6 | |
| 7 | import os |
| 8 | import sys |
| 9 | import subprocess |
| 10 | import argparse |
| 11 | import logging |
| 12 | |
| 13 | OPENSTACK_GET_SERVER_IPS = 'openstack server list --name "^(%s-).*" -c Name -c Networks -f value --sort-column Name' |
| 14 | |
| 15 | SSH_CMD_TEMPLATE = 'ssh -o StrictHostKeychecking=no -i %s ubuntu@%s "sudo su -c \\"%s\\""' |
| 16 | |
| 17 | KUBECTL_GET_ALL_POD_IMAGES_AND_SHAS = 'kubectl get pods --all-namespaces -o=jsonpath=\'{range .items[*]}' \ |
| 18 | '{\\\\\\"\\n\\\\\\"}{.metadata.name}{\\\\\\":::\\\\\\"}' \ |
| 19 | '{range .status.containerStatuses[*]}{.image}{\\\\\\"___\\\\\\"}' \ |
| 20 | '{.imageID}{\\\\\\" \\\\\\"}{end}{end}{\\\\\\"\\n\\\\\\"}\'' |
| 21 | |
| 22 | DOCKER_INSPECT = 'docker inspect --format=\'{{index .RepoTags 0}}{{\\\\\\" \\\\\\"}}{{index .RepoDigests 0}}\' ' \ |
| 23 | '\$(docker images -q | uniq| tr \'\n\' \' \')' |
| 24 | |
| 25 | KUBECTL_VERSION = 'kubectl version' |
| 26 | DOCKER_VERSION = 'docker --version' |
| 27 | |
AndyWalshe | 3e92cbf | 2019-10-08 16:33:14 +0100 | [diff] [blame^] | 28 | local_registry = "" |
| 29 | |
AndyWalshe | c691f02 | 2019-06-10 23:20:41 +0000 | [diff] [blame] | 30 | logging.basicConfig(level=logging.DEBUG, format='%(message)s') |
| 31 | file_log = logging.FileHandler(filename='onap-probe-report.txt', mode='w') |
| 32 | file_log.setLevel(logging.INFO) |
| 33 | formatter = logging.Formatter('%(message)s') |
| 34 | file_log.setFormatter(formatter) |
| 35 | logging.getLogger('').addHandler(file_log) |
| 36 | |
| 37 | |
| 38 | class CommandResult(object): |
| 39 | def __init__(self, exit_code, stdout, stderr): |
| 40 | self.exit_code = exit_code |
| 41 | self.stdout = stdout |
| 42 | self.stderr = stderr |
| 43 | |
| 44 | |
| 45 | def run_command_or_exit(command, message=""): |
| 46 | if message: |
| 47 | logging.debug(message) |
| 48 | logging.debug('cmd> ' + command) |
| 49 | |
| 50 | child = subprocess.Popen(command, stdout=subprocess.PIPE, |
| 51 | stderr=subprocess.PIPE, shell=True) |
| 52 | result = child.communicate() |
| 53 | |
| 54 | cmd_result = CommandResult(child.returncode, |
| 55 | result[0].strip(), result[1].strip()) |
| 56 | |
| 57 | if cmd_result.exit_code: |
| 58 | logging.error("exit_code: '%d', stdout: '%s', stderr: '%s'" % |
| 59 | (cmd_result.exit_code, cmd_result.stdout, cmd_result.stderr)) |
| 60 | sys.exit(1) |
| 61 | |
| 62 | return cmd_result |
| 63 | |
| 64 | |
| 65 | class OpenStackK8sCluster(object): |
| 66 | def __init__(self, stack_name, identity_file): |
| 67 | self.stack_name = stack_name |
| 68 | self.identity_file = identity_file |
| 69 | self.servers = {} |
| 70 | self.vm_docker_images = set() |
| 71 | self.kubectl_version = 'unknown' |
| 72 | self.docker_version = 'unknown' |
| 73 | |
| 74 | response = run_command_or_exit(OPENSTACK_GET_SERVER_IPS % stack_name, |
| 75 | "Get all stack servers and ip addressed using stack name").stdout |
| 76 | for line in response.split('\n'): |
| 77 | parts = line.split() |
| 78 | self.servers[parts[0].replace(stack_name+'-', "")] = parts[2] |
| 79 | |
| 80 | def __str__(self): |
| 81 | desc = "Stack name: " + self.stack_name + '\n' |
| 82 | for key, value in sorted(self.servers.items()): |
| 83 | desc += " " + key + " : " + value + "\n" |
| 84 | return desc.strip() |
| 85 | |
| 86 | def get_stack_name(self): |
| 87 | return self.stack_name |
| 88 | |
| 89 | def get_identity_file(self): |
| 90 | return self.identity_file |
| 91 | |
AndyWalshe | 3e92cbf | 2019-10-08 16:33:14 +0100 | [diff] [blame^] | 92 | def get_nfs_ip_address(self): |
| 93 | return self.servers['nfs'] |
AndyWalshe | c691f02 | 2019-06-10 23:20:41 +0000 | [diff] [blame] | 94 | |
| 95 | def get_worker_nodes(self): |
| 96 | return [value for key, value in self.servers.items() if 'k8s-' in key.lower()] |
| 97 | |
| 98 | def get_orchestrators(self): |
| 99 | return [value for key, value in self.servers.items() if 'orch-' in key.lower()] |
| 100 | |
| 101 | def determine_docker_images_on_vms(self): |
| 102 | for node_ip in self.get_worker_nodes() + self.get_orchestrators(): |
| 103 | command = SSH_CMD_TEMPLATE % (self.get_identity_file(), node_ip, DOCKER_INSPECT) |
| 104 | vm_inspect_results = run_command_or_exit(command, "Examine server and list docker images").stdout |
| 105 | |
| 106 | for inspect_line in vm_inspect_results.split('\n'): |
| 107 | name_tag, name_digest = inspect_line.split(' ') |
| 108 | name, tag = name_tag.rsplit(':', 1) |
| 109 | digest = name_digest.split('sha256:')[1] |
AndyWalshe | 3e92cbf | 2019-10-08 16:33:14 +0100 | [diff] [blame^] | 110 | if local_registry: |
| 111 | name = name.replace(local_registry + "/", "") |
AndyWalshe | c691f02 | 2019-06-10 23:20:41 +0000 | [diff] [blame] | 112 | self.vm_docker_images.add((name, tag, digest)) |
| 113 | |
| 114 | def get_docker_images_on_vms(self): |
| 115 | return self.vm_docker_images |
| 116 | |
| 117 | def get_number_of_vm_docker_images(self): |
| 118 | return len(self.vm_docker_images) |
| 119 | |
| 120 | def determine_kubectl_version(self): |
AndyWalshe | 3e92cbf | 2019-10-08 16:33:14 +0100 | [diff] [blame^] | 121 | command = SSH_CMD_TEMPLATE % (self.get_identity_file(), self.get_nfs_ip_address(), KUBECTL_VERSION) |
| 122 | self.kubectl_version = run_command_or_exit(command, "Examine nfs vm to determine kubectl version").stdout |
AndyWalshe | c691f02 | 2019-06-10 23:20:41 +0000 | [diff] [blame] | 123 | |
| 124 | def get_kubectl_version(self): |
| 125 | return self.kubectl_version |
| 126 | |
| 127 | def determine_docker_version(self): |
| 128 | command = SSH_CMD_TEMPLATE % (self.get_identity_file(), self.get_worker_nodes()[0], DOCKER_VERSION) |
| 129 | self.docker_version = run_command_or_exit(command, "Examine worker node to determine docker version").stdout |
| 130 | |
| 131 | def get_docker_version(self): |
| 132 | return self.docker_version |
| 133 | |
| 134 | |
| 135 | class OnapDeployment(object): |
| 136 | def __init__(self, openstack_stack): |
| 137 | self.stack = openstack_stack |
| 138 | self.raw = "" |
| 139 | self.pods = [] |
| 140 | self.unique_images = set() |
| 141 | |
| 142 | def dig(self): |
AndyWalshe | 3e92cbf | 2019-10-08 16:33:14 +0100 | [diff] [blame^] | 143 | command = SSH_CMD_TEMPLATE % (self.stack.get_identity_file(), self.stack.get_nfs_ip_address(), |
AndyWalshe | c691f02 | 2019-06-10 23:20:41 +0000 | [diff] [blame] | 144 | KUBECTL_GET_ALL_POD_IMAGES_AND_SHAS) |
| 145 | self.raw = run_command_or_exit(command, "Use kubectl to retrieve all pods and pod images in K8S cluster").stdout |
| 146 | |
| 147 | for row in self.raw.strip().split("\n"): |
| 148 | self.pods.append(Pod(row)) |
| 149 | |
| 150 | for pod in self.pods: |
| 151 | for image in pod.get_images(): |
| 152 | self.unique_images.add(image) |
| 153 | |
| 154 | def __str__(self): |
| 155 | desc = "Pods and docker images:\n" |
| 156 | for pod in sorted(self.pods): |
| 157 | desc += str(pod) |
| 158 | return desc.strip() |
| 159 | |
| 160 | def get_number_of_pods(self): |
| 161 | return len(self.pods) |
| 162 | |
| 163 | def get_docker_images(self): |
| 164 | return sorted(self.unique_images) |
| 165 | |
| 166 | def get_number_of_unique_docker_images(self): |
| 167 | return len(self.unique_images) |
| 168 | |
| 169 | def get_number_of_docker_images(self): |
| 170 | images = [] |
| 171 | for pod in self.pods: |
| 172 | for image in pod.get_images(): |
| 173 | images.append(image) |
| 174 | return len(images) |
| 175 | |
| 176 | |
| 177 | class Pod(object): |
| 178 | def __init__(self, data): |
| 179 | self.name, images = data.strip().split(":::") |
| 180 | self.shas_images = {} |
| 181 | for item in images.split(" "): |
| 182 | image_raw, sha_raw = item.split("___") |
AndyWalshe | 3e92cbf | 2019-10-08 16:33:14 +0100 | [diff] [blame^] | 183 | if local_registry: |
| 184 | image_raw = image_raw.replace(local_registry + "/", "") |
AndyWalshe | c691f02 | 2019-06-10 23:20:41 +0000 | [diff] [blame] | 185 | if "sha256:" in images: |
| 186 | self.shas_images[sha_raw.split("sha256:")[1]] = image_raw |
| 187 | |
| 188 | def get_images(self): |
| 189 | return self.shas_images.values() |
| 190 | |
| 191 | def __cmp__(self, other): |
| 192 | return cmp(self.name, other.name) |
| 193 | |
| 194 | def __str__(self): |
| 195 | desc = self.name + "\n" |
| 196 | for key, value in sorted(self.shas_images.items(), key=lambda x: x[1]): |
| 197 | desc += " " + value + ", " + key + "\n" |
| 198 | return desc |
| 199 | |
| 200 | |
| 201 | def main(): |
| 202 | # Disable output buffering to receive the output instantly |
| 203 | sys.stdout = os.fdopen(sys.stdout.fileno(), "w", 0) |
| 204 | sys.stderr = os.fdopen(sys.stderr.fileno(), "w", 0) |
| 205 | |
| 206 | help_desc = "Script to probe an ONAP k8s cluster on an OpenStack stack and report back information on the " \ |
| 207 | "stack vms, all pods and corresponding docker image versions/digests and some of the tool versions " \ |
| 208 | "utilised." |
| 209 | parser = argparse.ArgumentParser(description=help_desc) |
| 210 | parser.add_argument("-s", "--stack-name", dest="stack_name", |
| 211 | help="OpenStack stack name used by this ONAP deployment", |
| 212 | metavar="STACKNAME", required=True) |
| 213 | parser.add_argument("-i", "--identity_file", dest="identity_file", |
| 214 | help="OpenStack identity file to be used by ssh to access servers", |
| 215 | metavar="IDENTITY-FILE", required=True) |
AndyWalshe | 3e92cbf | 2019-10-08 16:33:14 +0100 | [diff] [blame^] | 216 | parser.add_argument("-r", "--registry", dest="registry", |
| 217 | help="Local registry used to serve docker images which should be " + |
| 218 | " stripped from any image names in the script output", |
| 219 | metavar="REGISTRY", required=False) |
AndyWalshe | c691f02 | 2019-06-10 23:20:41 +0000 | [diff] [blame] | 220 | args = parser.parse_args() |
AndyWalshe | 3e92cbf | 2019-10-08 16:33:14 +0100 | [diff] [blame^] | 221 | if args.registry: |
| 222 | global local_registry |
| 223 | local_registry = args.registry |
AndyWalshe | c691f02 | 2019-06-10 23:20:41 +0000 | [diff] [blame] | 224 | |
| 225 | openstack_k8s = OpenStackK8sCluster(args.stack_name, args.identity_file) |
| 226 | openstack_k8s.determine_kubectl_version() |
| 227 | openstack_k8s.determine_docker_version() |
| 228 | openstack_k8s.determine_docker_images_on_vms() |
| 229 | |
| 230 | onap_dep = OnapDeployment(openstack_k8s) |
| 231 | onap_dep.dig() |
| 232 | |
| 233 | logging.info('\n%s\n' % openstack_k8s) |
| 234 | logging.info("number of pods: %d" % onap_dep.get_number_of_pods()) |
| 235 | logging.info("number of docker images in pods: %d" % onap_dep.get_number_of_docker_images()) |
| 236 | logging.info("number of unique docker images in pods: %d" % onap_dep.get_number_of_unique_docker_images()) |
| 237 | logging.info("number of unique docker images on vms: %d" % openstack_k8s.get_number_of_vm_docker_images()) |
| 238 | logging.info("docker version:\n%s" % openstack_k8s.get_docker_version()) |
| 239 | logging.info("kubectl version:\n%s" % openstack_k8s.get_kubectl_version()) |
| 240 | |
| 241 | logging.info("\n%s\n" % onap_dep) |
| 242 | |
| 243 | logging.info("<image-name>,<image-version>,<image-digest>") |
| 244 | for entry in sorted(openstack_k8s.get_docker_images_on_vms()): |
| 245 | logging.info('%s,%s,%s' % (entry[0], entry[1], entry[2])) |
| 246 | |
| 247 | |
| 248 | if __name__ == "__main__": |
| 249 | main() |