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 | |
| 28 | logging.basicConfig(level=logging.DEBUG, format='%(message)s') |
| 29 | file_log = logging.FileHandler(filename='onap-probe-report.txt', mode='w') |
| 30 | file_log.setLevel(logging.INFO) |
| 31 | formatter = logging.Formatter('%(message)s') |
| 32 | file_log.setFormatter(formatter) |
| 33 | logging.getLogger('').addHandler(file_log) |
| 34 | |
| 35 | |
| 36 | class CommandResult(object): |
| 37 | def __init__(self, exit_code, stdout, stderr): |
| 38 | self.exit_code = exit_code |
| 39 | self.stdout = stdout |
| 40 | self.stderr = stderr |
| 41 | |
| 42 | |
| 43 | def run_command_or_exit(command, message=""): |
| 44 | if message: |
| 45 | logging.debug(message) |
| 46 | logging.debug('cmd> ' + command) |
| 47 | |
| 48 | child = subprocess.Popen(command, stdout=subprocess.PIPE, |
| 49 | stderr=subprocess.PIPE, shell=True) |
| 50 | result = child.communicate() |
| 51 | |
| 52 | cmd_result = CommandResult(child.returncode, |
| 53 | result[0].strip(), result[1].strip()) |
| 54 | |
| 55 | if cmd_result.exit_code: |
| 56 | logging.error("exit_code: '%d', stdout: '%s', stderr: '%s'" % |
| 57 | (cmd_result.exit_code, cmd_result.stdout, cmd_result.stderr)) |
| 58 | sys.exit(1) |
| 59 | |
| 60 | return cmd_result |
| 61 | |
| 62 | |
| 63 | class OpenStackK8sCluster(object): |
| 64 | def __init__(self, stack_name, identity_file): |
| 65 | self.stack_name = stack_name |
| 66 | self.identity_file = identity_file |
| 67 | self.servers = {} |
| 68 | self.vm_docker_images = set() |
| 69 | self.kubectl_version = 'unknown' |
| 70 | self.docker_version = 'unknown' |
| 71 | |
| 72 | response = run_command_or_exit(OPENSTACK_GET_SERVER_IPS % stack_name, |
| 73 | "Get all stack servers and ip addressed using stack name").stdout |
| 74 | for line in response.split('\n'): |
| 75 | parts = line.split() |
| 76 | self.servers[parts[0].replace(stack_name+'-', "")] = parts[2] |
| 77 | |
| 78 | def __str__(self): |
| 79 | desc = "Stack name: " + self.stack_name + '\n' |
| 80 | for key, value in sorted(self.servers.items()): |
| 81 | desc += " " + key + " : " + value + "\n" |
| 82 | return desc.strip() |
| 83 | |
| 84 | def get_stack_name(self): |
| 85 | return self.stack_name |
| 86 | |
| 87 | def get_identity_file(self): |
| 88 | return self.identity_file |
| 89 | |
| 90 | def get_rancher_ip_address(self): |
| 91 | return self.servers['rancher'] |
| 92 | |
| 93 | def get_worker_nodes(self): |
| 94 | return [value for key, value in self.servers.items() if 'k8s-' in key.lower()] |
| 95 | |
| 96 | def get_orchestrators(self): |
| 97 | return [value for key, value in self.servers.items() if 'orch-' in key.lower()] |
| 98 | |
| 99 | def determine_docker_images_on_vms(self): |
| 100 | for node_ip in self.get_worker_nodes() + self.get_orchestrators(): |
| 101 | command = SSH_CMD_TEMPLATE % (self.get_identity_file(), node_ip, DOCKER_INSPECT) |
| 102 | vm_inspect_results = run_command_or_exit(command, "Examine server and list docker images").stdout |
| 103 | |
| 104 | for inspect_line in vm_inspect_results.split('\n'): |
| 105 | name_tag, name_digest = inspect_line.split(' ') |
| 106 | name, tag = name_tag.rsplit(':', 1) |
| 107 | digest = name_digest.split('sha256:')[1] |
| 108 | self.vm_docker_images.add((name, tag, digest)) |
| 109 | |
| 110 | def get_docker_images_on_vms(self): |
| 111 | return self.vm_docker_images |
| 112 | |
| 113 | def get_number_of_vm_docker_images(self): |
| 114 | return len(self.vm_docker_images) |
| 115 | |
| 116 | def determine_kubectl_version(self): |
| 117 | command = SSH_CMD_TEMPLATE % (self.get_identity_file(), self.get_rancher_ip_address(), KUBECTL_VERSION) |
| 118 | self.kubectl_version = run_command_or_exit(command, "Examine rancher vm to determine kubectl version").stdout |
| 119 | |
| 120 | def get_kubectl_version(self): |
| 121 | return self.kubectl_version |
| 122 | |
| 123 | def determine_docker_version(self): |
| 124 | command = SSH_CMD_TEMPLATE % (self.get_identity_file(), self.get_worker_nodes()[0], DOCKER_VERSION) |
| 125 | self.docker_version = run_command_or_exit(command, "Examine worker node to determine docker version").stdout |
| 126 | |
| 127 | def get_docker_version(self): |
| 128 | return self.docker_version |
| 129 | |
| 130 | |
| 131 | class OnapDeployment(object): |
| 132 | def __init__(self, openstack_stack): |
| 133 | self.stack = openstack_stack |
| 134 | self.raw = "" |
| 135 | self.pods = [] |
| 136 | self.unique_images = set() |
| 137 | |
| 138 | def dig(self): |
| 139 | command = SSH_CMD_TEMPLATE % (self.stack.get_identity_file(), self.stack.get_rancher_ip_address(), |
| 140 | KUBECTL_GET_ALL_POD_IMAGES_AND_SHAS) |
| 141 | self.raw = run_command_or_exit(command, "Use kubectl to retrieve all pods and pod images in K8S cluster").stdout |
| 142 | |
| 143 | for row in self.raw.strip().split("\n"): |
| 144 | self.pods.append(Pod(row)) |
| 145 | |
| 146 | for pod in self.pods: |
| 147 | for image in pod.get_images(): |
| 148 | self.unique_images.add(image) |
| 149 | |
| 150 | def __str__(self): |
| 151 | desc = "Pods and docker images:\n" |
| 152 | for pod in sorted(self.pods): |
| 153 | desc += str(pod) |
| 154 | return desc.strip() |
| 155 | |
| 156 | def get_number_of_pods(self): |
| 157 | return len(self.pods) |
| 158 | |
| 159 | def get_docker_images(self): |
| 160 | return sorted(self.unique_images) |
| 161 | |
| 162 | def get_number_of_unique_docker_images(self): |
| 163 | return len(self.unique_images) |
| 164 | |
| 165 | def get_number_of_docker_images(self): |
| 166 | images = [] |
| 167 | for pod in self.pods: |
| 168 | for image in pod.get_images(): |
| 169 | images.append(image) |
| 170 | return len(images) |
| 171 | |
| 172 | |
| 173 | class Pod(object): |
| 174 | def __init__(self, data): |
| 175 | self.name, images = data.strip().split(":::") |
| 176 | self.shas_images = {} |
| 177 | for item in images.split(" "): |
| 178 | image_raw, sha_raw = item.split("___") |
| 179 | if "sha256:" in images: |
| 180 | self.shas_images[sha_raw.split("sha256:")[1]] = image_raw |
| 181 | |
| 182 | def get_images(self): |
| 183 | return self.shas_images.values() |
| 184 | |
| 185 | def __cmp__(self, other): |
| 186 | return cmp(self.name, other.name) |
| 187 | |
| 188 | def __str__(self): |
| 189 | desc = self.name + "\n" |
| 190 | for key, value in sorted(self.shas_images.items(), key=lambda x: x[1]): |
| 191 | desc += " " + value + ", " + key + "\n" |
| 192 | return desc |
| 193 | |
| 194 | |
| 195 | def main(): |
| 196 | # Disable output buffering to receive the output instantly |
| 197 | sys.stdout = os.fdopen(sys.stdout.fileno(), "w", 0) |
| 198 | sys.stderr = os.fdopen(sys.stderr.fileno(), "w", 0) |
| 199 | |
| 200 | help_desc = "Script to probe an ONAP k8s cluster on an OpenStack stack and report back information on the " \ |
| 201 | "stack vms, all pods and corresponding docker image versions/digests and some of the tool versions " \ |
| 202 | "utilised." |
| 203 | parser = argparse.ArgumentParser(description=help_desc) |
| 204 | parser.add_argument("-s", "--stack-name", dest="stack_name", |
| 205 | help="OpenStack stack name used by this ONAP deployment", |
| 206 | metavar="STACKNAME", required=True) |
| 207 | parser.add_argument("-i", "--identity_file", dest="identity_file", |
| 208 | help="OpenStack identity file to be used by ssh to access servers", |
| 209 | metavar="IDENTITY-FILE", required=True) |
| 210 | args = parser.parse_args() |
| 211 | |
| 212 | openstack_k8s = OpenStackK8sCluster(args.stack_name, args.identity_file) |
| 213 | openstack_k8s.determine_kubectl_version() |
| 214 | openstack_k8s.determine_docker_version() |
| 215 | openstack_k8s.determine_docker_images_on_vms() |
| 216 | |
| 217 | onap_dep = OnapDeployment(openstack_k8s) |
| 218 | onap_dep.dig() |
| 219 | |
| 220 | logging.info('\n%s\n' % openstack_k8s) |
| 221 | logging.info("number of pods: %d" % onap_dep.get_number_of_pods()) |
| 222 | logging.info("number of docker images in pods: %d" % onap_dep.get_number_of_docker_images()) |
| 223 | logging.info("number of unique docker images in pods: %d" % onap_dep.get_number_of_unique_docker_images()) |
| 224 | logging.info("number of unique docker images on vms: %d" % openstack_k8s.get_number_of_vm_docker_images()) |
| 225 | logging.info("docker version:\n%s" % openstack_k8s.get_docker_version()) |
| 226 | logging.info("kubectl version:\n%s" % openstack_k8s.get_kubectl_version()) |
| 227 | |
| 228 | logging.info("\n%s\n" % onap_dep) |
| 229 | |
| 230 | logging.info("<image-name>,<image-version>,<image-digest>") |
| 231 | for entry in sorted(openstack_k8s.get_docker_images_on_vms()): |
| 232 | logging.info('%s,%s,%s' % (entry[0], entry[1], entry[2])) |
| 233 | |
| 234 | |
| 235 | if __name__ == "__main__": |
| 236 | main() |