paasta-tools 1.21.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (348) hide show
  1. k8s_itests/__init__.py +0 -0
  2. k8s_itests/test_autoscaling.py +23 -0
  3. k8s_itests/utils.py +38 -0
  4. paasta_tools/__init__.py +20 -0
  5. paasta_tools/adhoc_tools.py +142 -0
  6. paasta_tools/api/__init__.py +13 -0
  7. paasta_tools/api/api.py +330 -0
  8. paasta_tools/api/api_docs/swagger.json +2323 -0
  9. paasta_tools/api/client.py +106 -0
  10. paasta_tools/api/settings.py +33 -0
  11. paasta_tools/api/tweens/__init__.py +6 -0
  12. paasta_tools/api/tweens/auth.py +125 -0
  13. paasta_tools/api/tweens/profiling.py +108 -0
  14. paasta_tools/api/tweens/request_logger.py +124 -0
  15. paasta_tools/api/views/__init__.py +13 -0
  16. paasta_tools/api/views/autoscaler.py +100 -0
  17. paasta_tools/api/views/exception.py +45 -0
  18. paasta_tools/api/views/flink.py +73 -0
  19. paasta_tools/api/views/instance.py +395 -0
  20. paasta_tools/api/views/pause_autoscaler.py +71 -0
  21. paasta_tools/api/views/remote_run.py +113 -0
  22. paasta_tools/api/views/resources.py +76 -0
  23. paasta_tools/api/views/service.py +35 -0
  24. paasta_tools/api/views/version.py +25 -0
  25. paasta_tools/apply_external_resources.py +79 -0
  26. paasta_tools/async_utils.py +109 -0
  27. paasta_tools/autoscaling/__init__.py +0 -0
  28. paasta_tools/autoscaling/autoscaling_service_lib.py +57 -0
  29. paasta_tools/autoscaling/forecasting.py +106 -0
  30. paasta_tools/autoscaling/max_all_k8s_services.py +41 -0
  31. paasta_tools/autoscaling/pause_service_autoscaler.py +77 -0
  32. paasta_tools/autoscaling/utils.py +52 -0
  33. paasta_tools/bounce_lib.py +184 -0
  34. paasta_tools/broadcast_log_to_services.py +62 -0
  35. paasta_tools/cassandracluster_tools.py +210 -0
  36. paasta_tools/check_autoscaler_max_instances.py +212 -0
  37. paasta_tools/check_cassandracluster_services_replication.py +35 -0
  38. paasta_tools/check_flink_services_health.py +203 -0
  39. paasta_tools/check_kubernetes_api.py +57 -0
  40. paasta_tools/check_kubernetes_services_replication.py +141 -0
  41. paasta_tools/check_oom_events.py +244 -0
  42. paasta_tools/check_services_replication_tools.py +324 -0
  43. paasta_tools/check_spark_jobs.py +234 -0
  44. paasta_tools/cleanup_kubernetes_cr.py +138 -0
  45. paasta_tools/cleanup_kubernetes_crd.py +145 -0
  46. paasta_tools/cleanup_kubernetes_jobs.py +344 -0
  47. paasta_tools/cleanup_tron_namespaces.py +96 -0
  48. paasta_tools/cli/__init__.py +13 -0
  49. paasta_tools/cli/authentication.py +85 -0
  50. paasta_tools/cli/cli.py +260 -0
  51. paasta_tools/cli/cmds/__init__.py +13 -0
  52. paasta_tools/cli/cmds/autoscale.py +143 -0
  53. paasta_tools/cli/cmds/check.py +334 -0
  54. paasta_tools/cli/cmds/cook_image.py +147 -0
  55. paasta_tools/cli/cmds/get_docker_image.py +76 -0
  56. paasta_tools/cli/cmds/get_image_version.py +172 -0
  57. paasta_tools/cli/cmds/get_latest_deployment.py +93 -0
  58. paasta_tools/cli/cmds/info.py +155 -0
  59. paasta_tools/cli/cmds/itest.py +117 -0
  60. paasta_tools/cli/cmds/list.py +66 -0
  61. paasta_tools/cli/cmds/list_clusters.py +42 -0
  62. paasta_tools/cli/cmds/list_deploy_queue.py +171 -0
  63. paasta_tools/cli/cmds/list_namespaces.py +84 -0
  64. paasta_tools/cli/cmds/local_run.py +1396 -0
  65. paasta_tools/cli/cmds/logs.py +1601 -0
  66. paasta_tools/cli/cmds/mark_for_deployment.py +1988 -0
  67. paasta_tools/cli/cmds/mesh_status.py +174 -0
  68. paasta_tools/cli/cmds/pause_service_autoscaler.py +107 -0
  69. paasta_tools/cli/cmds/push_to_registry.py +275 -0
  70. paasta_tools/cli/cmds/remote_run.py +252 -0
  71. paasta_tools/cli/cmds/rollback.py +347 -0
  72. paasta_tools/cli/cmds/secret.py +549 -0
  73. paasta_tools/cli/cmds/security_check.py +59 -0
  74. paasta_tools/cli/cmds/spark_run.py +1400 -0
  75. paasta_tools/cli/cmds/start_stop_restart.py +401 -0
  76. paasta_tools/cli/cmds/status.py +2302 -0
  77. paasta_tools/cli/cmds/validate.py +1012 -0
  78. paasta_tools/cli/cmds/wait_for_deployment.py +275 -0
  79. paasta_tools/cli/fsm/__init__.py +13 -0
  80. paasta_tools/cli/fsm/autosuggest.py +82 -0
  81. paasta_tools/cli/fsm/template/README.md +8 -0
  82. paasta_tools/cli/fsm/template/cookiecutter.json +7 -0
  83. paasta_tools/cli/fsm/template/{{cookiecutter.service}}/kubernetes-PROD.yaml +91 -0
  84. paasta_tools/cli/fsm/template/{{cookiecutter.service}}/monitoring.yaml +20 -0
  85. paasta_tools/cli/fsm/template/{{cookiecutter.service}}/service.yaml +8 -0
  86. paasta_tools/cli/fsm/template/{{cookiecutter.service}}/smartstack.yaml +6 -0
  87. paasta_tools/cli/fsm_cmd.py +121 -0
  88. paasta_tools/cli/paasta_tabcomplete.sh +23 -0
  89. paasta_tools/cli/schemas/adhoc_schema.json +199 -0
  90. paasta_tools/cli/schemas/autoscaling_schema.json +91 -0
  91. paasta_tools/cli/schemas/autotuned_defaults/cassandracluster_schema.json +37 -0
  92. paasta_tools/cli/schemas/autotuned_defaults/kubernetes_schema.json +89 -0
  93. paasta_tools/cli/schemas/deploy_schema.json +173 -0
  94. paasta_tools/cli/schemas/eks_schema.json +970 -0
  95. paasta_tools/cli/schemas/kubernetes_schema.json +970 -0
  96. paasta_tools/cli/schemas/rollback_schema.json +160 -0
  97. paasta_tools/cli/schemas/service_schema.json +25 -0
  98. paasta_tools/cli/schemas/smartstack_schema.json +322 -0
  99. paasta_tools/cli/schemas/tron_schema.json +699 -0
  100. paasta_tools/cli/utils.py +1118 -0
  101. paasta_tools/clusterman.py +21 -0
  102. paasta_tools/config_utils.py +385 -0
  103. paasta_tools/contrib/__init__.py +0 -0
  104. paasta_tools/contrib/bounce_log_latency_parser.py +68 -0
  105. paasta_tools/contrib/check_manual_oapi_changes.sh +24 -0
  106. paasta_tools/contrib/check_orphans.py +306 -0
  107. paasta_tools/contrib/create_dynamodb_table.py +35 -0
  108. paasta_tools/contrib/create_paasta_playground.py +105 -0
  109. paasta_tools/contrib/emit_allocated_cpu_metrics.py +50 -0
  110. paasta_tools/contrib/get_running_task_allocation.py +346 -0
  111. paasta_tools/contrib/habitat_fixer.py +86 -0
  112. paasta_tools/contrib/ide_helper.py +316 -0
  113. paasta_tools/contrib/is_pod_healthy_in_proxy.py +139 -0
  114. paasta_tools/contrib/is_pod_healthy_in_smartstack.py +50 -0
  115. paasta_tools/contrib/kill_bad_containers.py +109 -0
  116. paasta_tools/contrib/mass-deploy-tag.sh +44 -0
  117. paasta_tools/contrib/mock_patch_checker.py +86 -0
  118. paasta_tools/contrib/paasta_update_soa_memcpu.py +520 -0
  119. paasta_tools/contrib/render_template.py +129 -0
  120. paasta_tools/contrib/rightsizer_soaconfigs_update.py +348 -0
  121. paasta_tools/contrib/service_shard_remove.py +157 -0
  122. paasta_tools/contrib/service_shard_update.py +373 -0
  123. paasta_tools/contrib/shared_ip_check.py +77 -0
  124. paasta_tools/contrib/timeouts_metrics_prom.py +64 -0
  125. paasta_tools/delete_kubernetes_deployments.py +89 -0
  126. paasta_tools/deployment_utils.py +44 -0
  127. paasta_tools/docker_wrapper.py +234 -0
  128. paasta_tools/docker_wrapper_imports.py +13 -0
  129. paasta_tools/drain_lib.py +351 -0
  130. paasta_tools/dump_locally_running_services.py +71 -0
  131. paasta_tools/eks_tools.py +119 -0
  132. paasta_tools/envoy_tools.py +373 -0
  133. paasta_tools/firewall.py +504 -0
  134. paasta_tools/firewall_logging.py +154 -0
  135. paasta_tools/firewall_update.py +172 -0
  136. paasta_tools/flink_tools.py +345 -0
  137. paasta_tools/flinkeks_tools.py +90 -0
  138. paasta_tools/frameworks/__init__.py +0 -0
  139. paasta_tools/frameworks/adhoc_scheduler.py +71 -0
  140. paasta_tools/frameworks/constraints.py +87 -0
  141. paasta_tools/frameworks/native_scheduler.py +652 -0
  142. paasta_tools/frameworks/native_service_config.py +301 -0
  143. paasta_tools/frameworks/task_store.py +245 -0
  144. paasta_tools/generate_all_deployments +9 -0
  145. paasta_tools/generate_authenticating_services.py +94 -0
  146. paasta_tools/generate_deployments_for_service.py +255 -0
  147. paasta_tools/generate_services_file.py +114 -0
  148. paasta_tools/generate_services_yaml.py +30 -0
  149. paasta_tools/hacheck.py +76 -0
  150. paasta_tools/instance/__init__.py +0 -0
  151. paasta_tools/instance/hpa_metrics_parser.py +122 -0
  152. paasta_tools/instance/kubernetes.py +1362 -0
  153. paasta_tools/iptables.py +240 -0
  154. paasta_tools/kafkacluster_tools.py +143 -0
  155. paasta_tools/kubernetes/__init__.py +0 -0
  156. paasta_tools/kubernetes/application/__init__.py +0 -0
  157. paasta_tools/kubernetes/application/controller_wrappers.py +476 -0
  158. paasta_tools/kubernetes/application/tools.py +90 -0
  159. paasta_tools/kubernetes/bin/__init__.py +0 -0
  160. paasta_tools/kubernetes/bin/kubernetes_remove_evicted_pods.py +164 -0
  161. paasta_tools/kubernetes/bin/paasta_cleanup_remote_run_resources.py +135 -0
  162. paasta_tools/kubernetes/bin/paasta_cleanup_stale_nodes.py +181 -0
  163. paasta_tools/kubernetes/bin/paasta_secrets_sync.py +758 -0
  164. paasta_tools/kubernetes/remote_run.py +558 -0
  165. paasta_tools/kubernetes_tools.py +4679 -0
  166. paasta_tools/list_kubernetes_service_instances.py +128 -0
  167. paasta_tools/list_tron_namespaces.py +60 -0
  168. paasta_tools/long_running_service_tools.py +678 -0
  169. paasta_tools/mac_address.py +44 -0
  170. paasta_tools/marathon_dashboard.py +0 -0
  171. paasta_tools/mesos/__init__.py +0 -0
  172. paasta_tools/mesos/cfg.py +46 -0
  173. paasta_tools/mesos/cluster.py +60 -0
  174. paasta_tools/mesos/exceptions.py +59 -0
  175. paasta_tools/mesos/framework.py +77 -0
  176. paasta_tools/mesos/log.py +48 -0
  177. paasta_tools/mesos/master.py +306 -0
  178. paasta_tools/mesos/mesos_file.py +169 -0
  179. paasta_tools/mesos/parallel.py +52 -0
  180. paasta_tools/mesos/slave.py +115 -0
  181. paasta_tools/mesos/task.py +94 -0
  182. paasta_tools/mesos/util.py +69 -0
  183. paasta_tools/mesos/zookeeper.py +37 -0
  184. paasta_tools/mesos_maintenance.py +848 -0
  185. paasta_tools/mesos_tools.py +1051 -0
  186. paasta_tools/metrics/__init__.py +0 -0
  187. paasta_tools/metrics/metastatus_lib.py +1110 -0
  188. paasta_tools/metrics/metrics_lib.py +217 -0
  189. paasta_tools/monitoring/__init__.py +13 -0
  190. paasta_tools/monitoring/check_k8s_api_performance.py +110 -0
  191. paasta_tools/monitoring_tools.py +652 -0
  192. paasta_tools/monkrelaycluster_tools.py +146 -0
  193. paasta_tools/nrtsearchservice_tools.py +143 -0
  194. paasta_tools/nrtsearchserviceeks_tools.py +68 -0
  195. paasta_tools/oom_logger.py +321 -0
  196. paasta_tools/paasta_deploy_tron_jobs +3 -0
  197. paasta_tools/paasta_execute_docker_command.py +123 -0
  198. paasta_tools/paasta_native_serviceinit.py +21 -0
  199. paasta_tools/paasta_service_config_loader.py +201 -0
  200. paasta_tools/paastaapi/__init__.py +29 -0
  201. paasta_tools/paastaapi/api/__init__.py +3 -0
  202. paasta_tools/paastaapi/api/autoscaler_api.py +302 -0
  203. paasta_tools/paastaapi/api/default_api.py +569 -0
  204. paasta_tools/paastaapi/api/remote_run_api.py +604 -0
  205. paasta_tools/paastaapi/api/resources_api.py +157 -0
  206. paasta_tools/paastaapi/api/service_api.py +1736 -0
  207. paasta_tools/paastaapi/api_client.py +818 -0
  208. paasta_tools/paastaapi/apis/__init__.py +22 -0
  209. paasta_tools/paastaapi/configuration.py +455 -0
  210. paasta_tools/paastaapi/exceptions.py +137 -0
  211. paasta_tools/paastaapi/model/__init__.py +5 -0
  212. paasta_tools/paastaapi/model/adhoc_launch_history.py +176 -0
  213. paasta_tools/paastaapi/model/autoscaler_count_msg.py +176 -0
  214. paasta_tools/paastaapi/model/deploy_queue.py +178 -0
  215. paasta_tools/paastaapi/model/deploy_queue_service_instance.py +194 -0
  216. paasta_tools/paastaapi/model/envoy_backend.py +185 -0
  217. paasta_tools/paastaapi/model/envoy_location.py +184 -0
  218. paasta_tools/paastaapi/model/envoy_status.py +181 -0
  219. paasta_tools/paastaapi/model/flink_cluster_overview.py +188 -0
  220. paasta_tools/paastaapi/model/flink_config.py +173 -0
  221. paasta_tools/paastaapi/model/flink_job.py +186 -0
  222. paasta_tools/paastaapi/model/flink_job_details.py +192 -0
  223. paasta_tools/paastaapi/model/flink_jobs.py +175 -0
  224. paasta_tools/paastaapi/model/float_and_error.py +173 -0
  225. paasta_tools/paastaapi/model/hpa_metric.py +176 -0
  226. paasta_tools/paastaapi/model/inline_object.py +170 -0
  227. paasta_tools/paastaapi/model/inline_response200.py +170 -0
  228. paasta_tools/paastaapi/model/inline_response2001.py +170 -0
  229. paasta_tools/paastaapi/model/instance_bounce_status.py +200 -0
  230. paasta_tools/paastaapi/model/instance_mesh_status.py +186 -0
  231. paasta_tools/paastaapi/model/instance_status.py +220 -0
  232. paasta_tools/paastaapi/model/instance_status_adhoc.py +187 -0
  233. paasta_tools/paastaapi/model/instance_status_cassandracluster.py +173 -0
  234. paasta_tools/paastaapi/model/instance_status_flink.py +173 -0
  235. paasta_tools/paastaapi/model/instance_status_kafkacluster.py +173 -0
  236. paasta_tools/paastaapi/model/instance_status_kubernetes.py +263 -0
  237. paasta_tools/paastaapi/model/instance_status_kubernetes_autoscaling_status.py +187 -0
  238. paasta_tools/paastaapi/model/instance_status_kubernetes_v2.py +197 -0
  239. paasta_tools/paastaapi/model/instance_status_tron.py +204 -0
  240. paasta_tools/paastaapi/model/instance_tasks.py +182 -0
  241. paasta_tools/paastaapi/model/integer_and_error.py +173 -0
  242. paasta_tools/paastaapi/model/kubernetes_container.py +178 -0
  243. paasta_tools/paastaapi/model/kubernetes_container_v2.py +219 -0
  244. paasta_tools/paastaapi/model/kubernetes_healthcheck.py +176 -0
  245. paasta_tools/paastaapi/model/kubernetes_pod.py +201 -0
  246. paasta_tools/paastaapi/model/kubernetes_pod_event.py +176 -0
  247. paasta_tools/paastaapi/model/kubernetes_pod_v2.py +213 -0
  248. paasta_tools/paastaapi/model/kubernetes_replica_set.py +185 -0
  249. paasta_tools/paastaapi/model/kubernetes_version.py +202 -0
  250. paasta_tools/paastaapi/model/remote_run_outcome.py +189 -0
  251. paasta_tools/paastaapi/model/remote_run_start.py +185 -0
  252. paasta_tools/paastaapi/model/remote_run_stop.py +176 -0
  253. paasta_tools/paastaapi/model/remote_run_token.py +173 -0
  254. paasta_tools/paastaapi/model/resource.py +187 -0
  255. paasta_tools/paastaapi/model/resource_item.py +187 -0
  256. paasta_tools/paastaapi/model/resource_value.py +176 -0
  257. paasta_tools/paastaapi/model/smartstack_backend.py +191 -0
  258. paasta_tools/paastaapi/model/smartstack_location.py +181 -0
  259. paasta_tools/paastaapi/model/smartstack_status.py +181 -0
  260. paasta_tools/paastaapi/model/task_tail_lines.py +176 -0
  261. paasta_tools/paastaapi/model_utils.py +1879 -0
  262. paasta_tools/paastaapi/models/__init__.py +62 -0
  263. paasta_tools/paastaapi/rest.py +287 -0
  264. paasta_tools/prune_completed_pods.py +220 -0
  265. paasta_tools/puppet_service_tools.py +59 -0
  266. paasta_tools/py.typed +1 -0
  267. paasta_tools/remote_git.py +127 -0
  268. paasta_tools/run-paasta-api-in-dev-mode.py +57 -0
  269. paasta_tools/run-paasta-api-playground.py +51 -0
  270. paasta_tools/secret_providers/__init__.py +66 -0
  271. paasta_tools/secret_providers/vault.py +214 -0
  272. paasta_tools/secret_tools.py +277 -0
  273. paasta_tools/setup_istio_mesh.py +353 -0
  274. paasta_tools/setup_kubernetes_cr.py +412 -0
  275. paasta_tools/setup_kubernetes_crd.py +138 -0
  276. paasta_tools/setup_kubernetes_internal_crd.py +154 -0
  277. paasta_tools/setup_kubernetes_job.py +353 -0
  278. paasta_tools/setup_prometheus_adapter_config.py +1028 -0
  279. paasta_tools/setup_tron_namespace.py +248 -0
  280. paasta_tools/slack.py +75 -0
  281. paasta_tools/smartstack_tools.py +676 -0
  282. paasta_tools/spark_tools.py +283 -0
  283. paasta_tools/synapse_srv_namespaces_fact.py +42 -0
  284. paasta_tools/tron/__init__.py +0 -0
  285. paasta_tools/tron/client.py +158 -0
  286. paasta_tools/tron/tron_command_context.py +194 -0
  287. paasta_tools/tron/tron_timeutils.py +101 -0
  288. paasta_tools/tron_tools.py +1448 -0
  289. paasta_tools/utils.py +4307 -0
  290. paasta_tools/yaml_tools.py +44 -0
  291. paasta_tools-1.21.3.data/scripts/apply_external_resources.py +79 -0
  292. paasta_tools-1.21.3.data/scripts/bounce_log_latency_parser.py +68 -0
  293. paasta_tools-1.21.3.data/scripts/check_autoscaler_max_instances.py +212 -0
  294. paasta_tools-1.21.3.data/scripts/check_cassandracluster_services_replication.py +35 -0
  295. paasta_tools-1.21.3.data/scripts/check_flink_services_health.py +203 -0
  296. paasta_tools-1.21.3.data/scripts/check_kubernetes_api.py +57 -0
  297. paasta_tools-1.21.3.data/scripts/check_kubernetes_services_replication.py +141 -0
  298. paasta_tools-1.21.3.data/scripts/check_manual_oapi_changes.sh +24 -0
  299. paasta_tools-1.21.3.data/scripts/check_oom_events.py +244 -0
  300. paasta_tools-1.21.3.data/scripts/check_orphans.py +306 -0
  301. paasta_tools-1.21.3.data/scripts/check_spark_jobs.py +234 -0
  302. paasta_tools-1.21.3.data/scripts/cleanup_kubernetes_cr.py +138 -0
  303. paasta_tools-1.21.3.data/scripts/cleanup_kubernetes_crd.py +145 -0
  304. paasta_tools-1.21.3.data/scripts/cleanup_kubernetes_jobs.py +344 -0
  305. paasta_tools-1.21.3.data/scripts/create_dynamodb_table.py +35 -0
  306. paasta_tools-1.21.3.data/scripts/create_paasta_playground.py +105 -0
  307. paasta_tools-1.21.3.data/scripts/delete_kubernetes_deployments.py +89 -0
  308. paasta_tools-1.21.3.data/scripts/emit_allocated_cpu_metrics.py +50 -0
  309. paasta_tools-1.21.3.data/scripts/generate_all_deployments +9 -0
  310. paasta_tools-1.21.3.data/scripts/generate_authenticating_services.py +94 -0
  311. paasta_tools-1.21.3.data/scripts/generate_deployments_for_service.py +255 -0
  312. paasta_tools-1.21.3.data/scripts/generate_services_file.py +114 -0
  313. paasta_tools-1.21.3.data/scripts/generate_services_yaml.py +30 -0
  314. paasta_tools-1.21.3.data/scripts/get_running_task_allocation.py +346 -0
  315. paasta_tools-1.21.3.data/scripts/habitat_fixer.py +86 -0
  316. paasta_tools-1.21.3.data/scripts/ide_helper.py +316 -0
  317. paasta_tools-1.21.3.data/scripts/is_pod_healthy_in_proxy.py +139 -0
  318. paasta_tools-1.21.3.data/scripts/is_pod_healthy_in_smartstack.py +50 -0
  319. paasta_tools-1.21.3.data/scripts/kill_bad_containers.py +109 -0
  320. paasta_tools-1.21.3.data/scripts/kubernetes_remove_evicted_pods.py +164 -0
  321. paasta_tools-1.21.3.data/scripts/mass-deploy-tag.sh +44 -0
  322. paasta_tools-1.21.3.data/scripts/mock_patch_checker.py +86 -0
  323. paasta_tools-1.21.3.data/scripts/paasta_cleanup_remote_run_resources.py +135 -0
  324. paasta_tools-1.21.3.data/scripts/paasta_cleanup_stale_nodes.py +181 -0
  325. paasta_tools-1.21.3.data/scripts/paasta_deploy_tron_jobs +3 -0
  326. paasta_tools-1.21.3.data/scripts/paasta_execute_docker_command.py +123 -0
  327. paasta_tools-1.21.3.data/scripts/paasta_secrets_sync.py +758 -0
  328. paasta_tools-1.21.3.data/scripts/paasta_tabcomplete.sh +23 -0
  329. paasta_tools-1.21.3.data/scripts/paasta_update_soa_memcpu.py +520 -0
  330. paasta_tools-1.21.3.data/scripts/render_template.py +129 -0
  331. paasta_tools-1.21.3.data/scripts/rightsizer_soaconfigs_update.py +348 -0
  332. paasta_tools-1.21.3.data/scripts/service_shard_remove.py +157 -0
  333. paasta_tools-1.21.3.data/scripts/service_shard_update.py +373 -0
  334. paasta_tools-1.21.3.data/scripts/setup_istio_mesh.py +353 -0
  335. paasta_tools-1.21.3.data/scripts/setup_kubernetes_cr.py +412 -0
  336. paasta_tools-1.21.3.data/scripts/setup_kubernetes_crd.py +138 -0
  337. paasta_tools-1.21.3.data/scripts/setup_kubernetes_internal_crd.py +154 -0
  338. paasta_tools-1.21.3.data/scripts/setup_kubernetes_job.py +353 -0
  339. paasta_tools-1.21.3.data/scripts/setup_prometheus_adapter_config.py +1028 -0
  340. paasta_tools-1.21.3.data/scripts/shared_ip_check.py +77 -0
  341. paasta_tools-1.21.3.data/scripts/synapse_srv_namespaces_fact.py +42 -0
  342. paasta_tools-1.21.3.data/scripts/timeouts_metrics_prom.py +64 -0
  343. paasta_tools-1.21.3.dist-info/LICENSE +201 -0
  344. paasta_tools-1.21.3.dist-info/METADATA +74 -0
  345. paasta_tools-1.21.3.dist-info/RECORD +348 -0
  346. paasta_tools-1.21.3.dist-info/WHEEL +5 -0
  347. paasta_tools-1.21.3.dist-info/entry_points.txt +20 -0
  348. paasta_tools-1.21.3.dist-info/top_level.txt +2 -0
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env python3.8
2
+ import ast
3
+ import sys
4
+
5
+
6
+ class MockChecker(ast.NodeVisitor):
7
+ def __init__(self):
8
+ self.errors = 0
9
+ self.init_module_imports()
10
+
11
+ def init_module_imports(self):
12
+ self.imported_patch = False
13
+ self.imported_mock = False
14
+
15
+ def check_files(self, files):
16
+ for file in files:
17
+ self.check_file(file)
18
+
19
+ def check_file(self, filename):
20
+ self.current_filename = filename
21
+ try:
22
+ with open(filename, "r") as fd:
23
+ try:
24
+ file_ast = ast.parse(fd.read())
25
+ except SyntaxError as error:
26
+ print("SyntaxError on file %s:%d" % (filename, error.lineno))
27
+ return
28
+ except IOError:
29
+ print("Error opening filename: %s" % filename)
30
+ return
31
+ self.init_module_imports()
32
+ self.visit(file_ast)
33
+
34
+ def _call_uses_patch(self, node):
35
+ try:
36
+ return node.func.id == "patch"
37
+ except AttributeError:
38
+ return False
39
+
40
+ def _call_uses_mock_patch(self, node):
41
+ try:
42
+ return node.func.value.id == "mock" and node.func.attr == "patch"
43
+ except AttributeError:
44
+ return False
45
+
46
+ def visit_Import(self, node):
47
+ if [name for name in node.names if "mock" == name.name]:
48
+ self.imported_mock = True
49
+
50
+ def visit_ImportFrom(self, node):
51
+ if node.module == "mock" and (
52
+ name for name in node.names if "patch" == name.name
53
+ ):
54
+ self.imported_patch = True
55
+
56
+ def visit_Call(self, node):
57
+ try:
58
+ if (self.imported_patch and self._call_uses_patch(node)) or (
59
+ self.imported_mock and self._call_uses_mock_patch(node)
60
+ ):
61
+ if not any(
62
+ [keyword for keyword in node.keywords if keyword.arg == "autospec"]
63
+ ):
64
+ print(
65
+ "%s:%d: Found a mock without an autospec!"
66
+ % (self.current_filename, node.lineno)
67
+ )
68
+ self.errors += 1
69
+ except AttributeError:
70
+ pass
71
+ self.generic_visit(node)
72
+
73
+
74
+ def main(filenames):
75
+ checker = MockChecker()
76
+ checker.check_files(filenames)
77
+ if checker.errors == 0:
78
+ sys.exit(0)
79
+ else:
80
+ print("You probably meant to specify 'autospec=True' in these tests.")
81
+ print("If you really don't want to, specify 'autospec=None'")
82
+ sys.exit(1)
83
+
84
+
85
+ if __name__ == "__main__":
86
+ main(sys.argv[1:])
@@ -0,0 +1,520 @@
1
+ #!/usr/bin/env python3
2
+ import argparse
3
+ import contextlib
4
+ import json
5
+ import logging
6
+ import os
7
+ import subprocess
8
+ import tempfile
9
+ import time
10
+ from http.client import HTTPConnection
11
+
12
+ import requests
13
+ import ruamel.yaml as yaml
14
+
15
+ from paasta_tools.utils import DEFAULT_SOA_CONFIGS_GIT_URL
16
+ from paasta_tools.utils import format_git_url
17
+ from paasta_tools.utils import load_system_paasta_config
18
+
19
+ requests_log = logging.getLogger("requests.packages.urllib3")
20
+ logging.basicConfig(level=logging.INFO)
21
+ log = logging.getLogger()
22
+
23
+
24
+ def parse_args():
25
+ parser = argparse.ArgumentParser(description="")
26
+ parser.add_argument(
27
+ "-s",
28
+ "--splunk-creds",
29
+ help="Service credentials for Splunk API, user:pass",
30
+ dest="splunk_creds",
31
+ required=True,
32
+ )
33
+ parser.add_argument(
34
+ "-f",
35
+ "--criteria-filter",
36
+ help="Filter Splunk search results criteria field. Default: *",
37
+ dest="criteria_filter",
38
+ required=False,
39
+ default="*",
40
+ )
41
+ parser.add_argument(
42
+ "-j",
43
+ "--jira-creds",
44
+ help="Service credentials for JIRA API, user:pass",
45
+ dest="jira_creds",
46
+ required=False,
47
+ )
48
+ parser.add_argument(
49
+ "-t",
50
+ "--ticket",
51
+ help="Create JIRA tickets for every service in corresponding project (not available in bulk mode)",
52
+ action="store_true",
53
+ dest="ticket",
54
+ default=False,
55
+ )
56
+ parser.add_argument(
57
+ "-r",
58
+ "--reviews",
59
+ help="Guess owners of each service and create reviews automatically",
60
+ action="store_true",
61
+ dest="create_reviews",
62
+ default=False,
63
+ )
64
+ parser.add_argument(
65
+ "-p",
66
+ "--publish-reviews",
67
+ help="Guess owners of each service and publish reviews automatically",
68
+ action="store_true",
69
+ dest="publish_reviews",
70
+ default=False,
71
+ )
72
+ parser.add_argument(
73
+ "-b",
74
+ "--bulk",
75
+ help="Patch all services in the report with only one code review",
76
+ action="store_true",
77
+ dest="bulk",
78
+ default=False,
79
+ )
80
+ parser.add_argument(
81
+ "--app",
82
+ help="Splunk app of the CSV file",
83
+ default="yelp_performance",
84
+ required=False,
85
+ dest="splunk_app",
86
+ )
87
+ parser.add_argument(
88
+ "-c",
89
+ "--csv-report",
90
+ help="Splunk csv file from which to pull data.",
91
+ required=True,
92
+ dest="csv_report",
93
+ )
94
+ parser.add_argument(
95
+ "-y",
96
+ "--yelpsoa-configs-dir",
97
+ help="Use provided existing yelpsoa-configs instead of cloning the repo in a temporary dir. Only avail with -b option",
98
+ dest="YELPSOA_DIR",
99
+ required=False,
100
+ )
101
+ parser.add_argument(
102
+ "-l",
103
+ "--local",
104
+ help="Do not create a branch. Implies -y and -b.",
105
+ action="store_true",
106
+ dest="no_branch",
107
+ default=False,
108
+ )
109
+ parser.add_argument(
110
+ "-v",
111
+ "--verbose",
112
+ help="Debug mode.",
113
+ action="store_true",
114
+ dest="verbose",
115
+ )
116
+
117
+ return parser.parse_args()
118
+
119
+
120
+ def tempdir():
121
+ tmp = tempfile.TemporaryDirectory(prefix="repo", dir="/nail/tmp")
122
+ log.debug(f"Created temp directory: {tmp.name}")
123
+ return tmp
124
+
125
+
126
+ @contextlib.contextmanager
127
+ def cwd(path):
128
+ pwd = os.getcwd()
129
+ os.chdir(path)
130
+ log.debug(f"Switching from directory {pwd} to {path}")
131
+ try:
132
+ yield
133
+ finally:
134
+ log.debug(f"Switching back from directory {path} to {pwd}")
135
+ os.chdir(pwd)
136
+
137
+
138
+ def get_report_from_splunk(creds, app, filename, criteria_filter):
139
+ """Expect a table containing at least the following fields:
140
+ criteria (<service> kubernetes-<cluster_name> <instance>)
141
+ service_owner (Optional)
142
+ project (Required to create tickets)
143
+ estimated_monthly_savings (Optional)
144
+ search_time (Unix time)
145
+ one of the following pairs:
146
+ - current_cpus and suggested_cpus
147
+ - current_mem and suggested_mem
148
+ - current_disk and suggested_disk
149
+ - suggested_hacheck_cpus
150
+ - suggested_cpu_burst_add
151
+ - suggested_min_instances
152
+ - suggested_max_instances
153
+ """
154
+ url = f"https://splunk-api.yelpcorp.com/servicesNS/nobody/{app}/search/jobs/export"
155
+ search = (
156
+ '| inputlookup {filename} | search criteria="{criteria_filter}"'
157
+ '| eval _time = search_time | where _time > relative_time(now(),"-7d")'
158
+ ).format(filename=filename, criteria_filter=criteria_filter)
159
+ log.debug(f"Sending this query to Splunk: {search}\n")
160
+ data = {"output_mode": "json", "search": search}
161
+ creds = creds.split(":")
162
+ resp = requests.post(url, data=data, auth=(creds[0], creds[1]))
163
+ resp_text = resp.text.split("\n")
164
+ log.info("Found {} services to rightsize".format(len(resp_text) - 1))
165
+ resp_text = [x for x in resp_text if x]
166
+ resp_text = [json.loads(x) for x in resp_text]
167
+ services_to_update = {}
168
+ for d in resp_text:
169
+ if "result" not in d:
170
+ raise ValueError(f"Splunk request didn't return any results: {resp_text}")
171
+ criteria = d["result"]["criteria"]
172
+ serv = {}
173
+ serv["service"] = criteria.split(" ")[0]
174
+ serv["cluster"] = criteria.split(" ")[1]
175
+ serv["instance"] = criteria.split(" ")[2]
176
+ serv["owner"] = d["result"].get("service_owner", "Unavailable")
177
+ serv["date"] = d["result"]["_time"].split(" ")[0]
178
+ serv["money"] = d["result"].get("estimated_monthly_savings", 0)
179
+ serv["project"] = d["result"].get("project", "Unavailable")
180
+ serv["cpus"] = d["result"].get("suggested_cpus")
181
+ serv["old_cpus"] = d["result"].get("current_cpus")
182
+ serv["mem"] = d["result"].get("suggested_mem")
183
+ serv["old_mem"] = d["result"].get("current_mem")
184
+ serv["disk"] = d["result"].get("suggested_disk")
185
+ serv["old_disk"] = d["result"].get("current_disk")
186
+ serv["min_instances"] = d["result"].get("suggested_min_instances")
187
+ serv["max_instances"] = d["result"].get("suggested_max_instances")
188
+ serv["hacheck_cpus"] = d["result"].get("suggested_hacheck_cpus")
189
+ serv["cpu_burst_add"] = d["result"].get("suggested_cpu_burst_add")
190
+ services_to_update[criteria] = serv
191
+
192
+ return {
193
+ "search": search,
194
+ "results": services_to_update,
195
+ }
196
+
197
+
198
+ def clone_in(target_dir, system_paasta_config=None):
199
+ if not system_paasta_config:
200
+ system_paasta_config = load_system_paasta_config()
201
+ repo_config = system_paasta_config.get_git_repo_config("yelpsoa-configs")
202
+
203
+ remote = format_git_url(
204
+ system_paasta_config.get_git_config()["git_user"],
205
+ repo_config.get("git_server", DEFAULT_SOA_CONFIGS_GIT_URL),
206
+ repo_config["repo_name"],
207
+ )
208
+ subprocess.check_call(("git", "clone", remote, target_dir))
209
+
210
+
211
+ def create_branch(branch_name):
212
+ subprocess.check_call(("git", "checkout", "master"))
213
+ subprocess.check_call(("git", "checkout", "-b", branch_name))
214
+
215
+
216
+ def bulk_commit(filenames, originating_search):
217
+ message = f"Rightsizer bulk update\n\nSplunk search:\n{originating_search}"
218
+ subprocess.check_call(["git", "add"] + filenames)
219
+ subprocess.check_call(("git", "commit", "-n", "-m", message))
220
+
221
+
222
+ def bulk_review(filenames, originating_search, publish=False):
223
+ reviewers = set(get_reviewers_in_group("right-sizer"))
224
+ for filename in filenames:
225
+ reviewers = reviewers.union(get_reviewers(filename))
226
+
227
+ reviewers_arg = " ".join(list(reviewers))
228
+ summary = "Rightsizer bulk update"
229
+ description = (
230
+ "This is an automated bulk review. It will be shipped automatically if a primary reviewer gives a shipit. If you think this should not be shipped, talk to one of the primary reviewers. \n\n"
231
+ "This review is based on results from the following Splunk search:\n"
232
+ f"{originating_search}"
233
+ )
234
+ review_cmd = [
235
+ "review-branch",
236
+ f"--summary={summary}",
237
+ f"--description={description}",
238
+ "--reviewers",
239
+ reviewers_arg,
240
+ "--server",
241
+ "https://reviewboard.yelpcorp.com",
242
+ ]
243
+ if publish:
244
+ review_cmd.append("-p")
245
+
246
+ subprocess.check_call(review_cmd)
247
+
248
+
249
+ def commit(filename, serv):
250
+ message = "Updating {} for {}provisioned cpu from {} to {} cpus".format(
251
+ filename, serv["state"], serv["old_cpus"], serv["cpus"]
252
+ )
253
+ log.debug(f"Commit {filename} with the following message: {message}")
254
+ subprocess.check_call(("git", "add", filename))
255
+ subprocess.check_call(("git", "commit", "-n", "-m", message))
256
+
257
+
258
+ def get_reviewers_in_group(group_name):
259
+ """Using rbt's target-groups argument overrides our configured default review groups.
260
+ So we'll expand the group into usernames and pass those users in the group individually.
261
+ """
262
+ rightsizer_reviewers = json.loads(
263
+ subprocess.check_output(
264
+ (
265
+ "rbt",
266
+ "api-get",
267
+ "--server",
268
+ "https://reviewboard.yelpcorp.com",
269
+ f"groups/{group_name}/users/",
270
+ )
271
+ ).decode("UTF-8")
272
+ )
273
+ return [user.get("username", "") for user in rightsizer_reviewers.get("users", {})]
274
+
275
+
276
+ def get_reviewers(filename):
277
+ recent_authors = set()
278
+ authors = (
279
+ subprocess.check_output(("git", "log", "--format=%ae", "--", filename))
280
+ .decode("UTF-8")
281
+ .splitlines()
282
+ )
283
+
284
+ authors = [x.split("@")[0] for x in authors]
285
+ for author in authors:
286
+ if "no-reply" in author:
287
+ continue
288
+ recent_authors.add(author)
289
+ if len(recent_authors) >= 3:
290
+ break
291
+ return recent_authors
292
+
293
+
294
+ def review(filename, summary, description, publish_review):
295
+ all_reviewers = get_reviewers(filename).union(get_reviewers_in_group("right-sizer"))
296
+ reviewers_arg = " ".join(all_reviewers)
297
+ publish_arg = "-p" if publish_review is True else "-d"
298
+ subprocess.check_call(
299
+ (
300
+ "review-branch",
301
+ f"--summary={summary}",
302
+ f"--description={description}",
303
+ publish_arg,
304
+ "--reviewers",
305
+ reviewers_arg,
306
+ "--server",
307
+ "https://reviewboard.yelpcorp.com",
308
+ )
309
+ )
310
+
311
+
312
+ def edit_soa_configs(filename, instance, cpu, mem, disk):
313
+ if os.path.islink(filename):
314
+ real_filename = os.path.realpath(filename)
315
+ os.remove(filename)
316
+ else:
317
+ real_filename = filename
318
+ try:
319
+ with open(real_filename, "r") as fi:
320
+ yams = fi.read()
321
+ yams = yams.replace("cpus: .", "cpus: 0.")
322
+ data = yaml.round_trip_load(yams, preserve_quotes=True)
323
+
324
+ instdict = data[instance]
325
+ if cpu:
326
+ instdict["cpus"] = float(cpu)
327
+ if mem:
328
+ mem = max(128, round(float(mem)))
329
+ instdict["mem"] = mem
330
+ if disk:
331
+ instdict["disk"] = round(float(disk))
332
+ out = yaml.round_trip_dump(data, width=120)
333
+
334
+ with open(filename, "w") as fi:
335
+ fi.write(out)
336
+ except FileNotFoundError:
337
+ log.exception(f"Could not find {filename}")
338
+ except KeyError:
339
+ log.exception(f"Error in {filename}. Will continue")
340
+
341
+
342
+ def create_jira_ticket(serv, creds, description, JIRA):
343
+ creds = creds.split(":")
344
+ options = {"server": "https://jira.yelpcorp.com"}
345
+ jira_cli = JIRA(options=options, basic_auth=(creds[0], creds[1])) # noqa: F821
346
+ jira_ticket = {}
347
+ # Sometimes a project has required fields we can't predict
348
+ try:
349
+ jira_ticket = {
350
+ "project": {"key": serv["project"]},
351
+ "description": description,
352
+ "issuetype": {"name": "Improvement"},
353
+ "labels": ["perf-watching", "paasta-rightsizer"],
354
+ "summary": "{s}.{i} in {c} may be {o}provisioned".format(
355
+ s=serv["service"],
356
+ i=serv["instance"],
357
+ c=serv["cluster"],
358
+ o=serv["state"],
359
+ ),
360
+ }
361
+ tick = jira_cli.create_issue(fields=jira_ticket)
362
+ except Exception:
363
+ jira_ticket["project"] = {"key": "PEOBS"}
364
+ jira_ticket["labels"].append(serv["service"])
365
+ tick = jira_cli.create_issue(fields=jira_ticket)
366
+ return tick.key
367
+
368
+
369
+ def _get_dashboard_qs_param(param, value):
370
+ # Some dashboards may ask for query string params like param=value, but not this provider.
371
+ return f"variables%5B%5D={param}%3D{param}:{value}"
372
+
373
+
374
+ def generate_ticket_content(serv):
375
+ cpus = float(serv["cpus"])
376
+ provisioned_state = "over"
377
+ if cpus > float(serv["old_cpus"]):
378
+ provisioned_state = "under"
379
+
380
+ serv["state"] = provisioned_state
381
+ ticket_desc = (
382
+ "This ticket and CR have been auto-generated to help keep PaaSTA right-sized."
383
+ "\nPEOBS will review this CR and give a shipit. Then an ops deputy from your team can merge"
384
+ " if these values look good for your service after review."
385
+ "\nOpen an issue with any concerns and someone from PEOBS will respond."
386
+ "\nWe suspect that {s}.{i} in {c} may have been {o}-provisioned"
387
+ " during the 1 week prior to {d}. It initially had {x} cpus, but based on the below dashboard,"
388
+ " we recommend {y} cpus."
389
+ "\n- Dashboard: https://y.yelpcorp.com/{o}provisioned?{cluster_param}&{service_param}&{instance_param}"
390
+ "\n- Service owner: {n}"
391
+ "\n- Estimated monthly excess cost: ${m}"
392
+ "\n\nFor more information and sizing examples for larger services:"
393
+ "\n- Runbook: https://y.yelpcorp.com/rb-provisioning-alert"
394
+ "\n- Alert owner: pe-observability@yelp.com"
395
+ ).format(
396
+ s=serv["service"],
397
+ c=serv["cluster"],
398
+ i=serv["instance"],
399
+ o=provisioned_state,
400
+ d=serv["date"],
401
+ n=serv["owner"],
402
+ m=serv["money"],
403
+ x=serv["old_cpus"],
404
+ y=serv["cpus"],
405
+ cluster_param=_get_dashboard_qs_param("paasta_cluster", serv["cluster"]),
406
+ service_param=_get_dashboard_qs_param("paasta_service", serv["service"]),
407
+ instance_param=_get_dashboard_qs_param("paasta_instance", serv["instance"]),
408
+ )
409
+ summary = f"Rightsizing {serv['service']}.{serv['instance']} in {serv['cluster']} to make it not have {provisioned_state}-provisioned cpu" # noqa: E501
410
+ return (summary, ticket_desc)
411
+
412
+
413
+ def bulk_rightsize(report, create_code_review, publish_code_review, create_new_branch):
414
+ if create_new_branch:
415
+ branch = "rightsize-bulk-{}".format(int(time.time()))
416
+ create_branch(branch)
417
+
418
+ filenames = []
419
+ for _, serv in report["results"].items():
420
+ filename = "{}/{}.yaml".format(serv["service"], serv["cluster"])
421
+ filenames.append(filename)
422
+ cpus = serv.get("cpus", None)
423
+ mem = serv.get("mem", None)
424
+ disk = serv.get("disk", None)
425
+ edit_soa_configs(filename, serv["instance"], cpus, mem, disk)
426
+ if create_code_review:
427
+ bulk_commit(filenames, report["search"])
428
+ bulk_review(filenames, report["search"], publish_code_review)
429
+
430
+
431
+ def individual_rightsize(
432
+ report, create_tickets, jira_creds, create_review, publish_review, JIRA
433
+ ):
434
+ for _, serv in report["results"].items():
435
+ filename = "{}/{}.yaml".format(serv["service"], serv["cluster"])
436
+ summary, ticket_desc = generate_ticket_content(serv)
437
+
438
+ if create_tickets is True:
439
+ branch = create_jira_ticket(serv, jira_creds, ticket_desc, JIRA)
440
+ else:
441
+ branch = "rightsize-{}".format(int(time.time() * 1000))
442
+
443
+ create_branch(branch)
444
+ cpus = serv.get("cpus", None)
445
+ mem = serv.get("mem", None)
446
+ disk = serv.get("disk", None)
447
+ edit_soa_configs(filename, serv["instance"], cpus, mem, disk)
448
+ try:
449
+ commit(filename, serv)
450
+ if create_review:
451
+ review(filename, summary, ticket_desc, publish_review)
452
+ except Exception:
453
+ log.exception(
454
+ (
455
+ "\nUnable to push changes to {f}. Check if {f} conforms to"
456
+ "yelpsoa-configs yaml rules. No review created. To see the"
457
+ "cpu suggestion for this service check {t}."
458
+ ).format(f=filename, t=branch)
459
+ )
460
+ continue
461
+
462
+
463
+ def main():
464
+ args = parse_args()
465
+
466
+ if args.verbose:
467
+ log.setLevel(logging.DEBUG)
468
+ requests_log.setLevel(logging.DEBUG)
469
+ HTTPConnection.debuglevel = 2
470
+ requests_log.propagate = True
471
+
472
+ # Safety checks
473
+ if args.no_branch and not args.YELPSOA_DIR:
474
+ log.error(
475
+ "You must specify --yelpsoa-configs-dir to work on if you use the --local option"
476
+ )
477
+ return False
478
+
479
+ if args.ticket:
480
+ if not args.jira_creds:
481
+ raise ValueError("No JIRA creds specified")
482
+ # Only import the jira module if we need too
483
+ from jira.client import JIRA # noqa: F401
484
+ else:
485
+ JIRA = None
486
+
487
+ report = get_report_from_splunk(
488
+ args.splunk_creds, args.splunk_app, args.csv_report, args.criteria_filter
489
+ )
490
+
491
+ tmpdir = tempdir() # Create a tmp dir even if we are not using it
492
+
493
+ working_dir = args.YELPSOA_DIR
494
+ system_paasta_config = load_system_paasta_config()
495
+ if working_dir is None:
496
+ # Working in a temporary directory
497
+ working_dir = os.path.join("rightsizer", tmpdir.name)
498
+ clone_in(working_dir, system_paasta_config=system_paasta_config)
499
+
500
+ with cwd(working_dir):
501
+ if args.bulk or args.no_branch:
502
+ log.info("Running in bulk mode")
503
+ bulk_rightsize(
504
+ report, args.create_reviews, args.publish_reviews, not args.no_branch
505
+ )
506
+ else:
507
+ individual_rightsize(
508
+ report,
509
+ args.ticket,
510
+ args.jira_creds,
511
+ args.create_reviews,
512
+ args.publish_reviews,
513
+ JIRA,
514
+ )
515
+
516
+ tmpdir.cleanup() # Cleanup any tmpdire used
517
+
518
+
519
+ if __name__ == "__main__":
520
+ main()