ops-cli 2.2.0__py3-none-any.whl → 2.3.0__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.
- ops/cli/config.py +37 -6
- ops/cli/inventory.py +1 -1
- ops/cli/ssh.py +19 -1
- ops/inventory/ec2inventory.py +90 -115
- ops/inventory/plugin/cns.py +7 -7
- ops/inventory/plugin/ec2.py +7 -5
- ops/inventory/sshconfig.py +1 -1
- ops/simplevault.py +116 -46
- ops/terraform/terraform_cmd_generator.py +7 -3
- {ops_cli-2.2.0.dist-info → ops_cli-2.3.0.dist-info}/METADATA +160 -144
- {ops_cli-2.2.0.dist-info → ops_cli-2.3.0.dist-info}/RECORD +15 -15
- {ops_cli-2.2.0.dist-info → ops_cli-2.3.0.dist-info}/WHEEL +1 -1
- {ops_cli-2.2.0.dist-info → ops_cli-2.3.0.dist-info}/LICENSE +0 -0
- {ops_cli-2.2.0.dist-info → ops_cli-2.3.0.dist-info}/entry_points.txt +0 -0
- {ops_cli-2.2.0.dist-info → ops_cli-2.3.0.dist-info}/top_level.txt +0 -0
ops/cli/config.py
CHANGED
|
@@ -8,21 +8,22 @@
|
|
|
8
8
|
# OF ANY KIND, either express or implied. See the License for the specific language
|
|
9
9
|
# governing permissions and limitations under the License.
|
|
10
10
|
|
|
11
|
-
import collections
|
|
12
11
|
import os
|
|
13
|
-
|
|
14
12
|
import yaml
|
|
15
13
|
|
|
16
14
|
from ansible.module_utils.common.collections import ImmutableDict
|
|
17
15
|
from ansible.parsing.dataloader import DataLoader
|
|
18
|
-
from ansible.plugins.loader import PluginLoader
|
|
19
16
|
from ansible.template import Templar
|
|
20
|
-
from ansible.utils.vars import
|
|
17
|
+
from ansible.utils.vars import combine_vars
|
|
21
18
|
from ansible.vars.manager import VariableManager
|
|
22
19
|
from ops.cli import display
|
|
23
20
|
from ansible import constants as C
|
|
24
21
|
from ansible import context
|
|
25
22
|
import logging
|
|
23
|
+
from ansible.errors import AnsibleOptionsError
|
|
24
|
+
from ansible.module_utils._text import to_text
|
|
25
|
+
from ansible.parsing.splitter import parse_kv
|
|
26
|
+
from collections.abc import MutableMapping
|
|
26
27
|
|
|
27
28
|
logger = logging.getLogger(__name__)
|
|
28
29
|
|
|
@@ -34,6 +35,38 @@ def get_cluster_name(cluster_config_path):
|
|
|
34
35
|
'/')[-1].replace('.yaml', '').replace('.yml', '')
|
|
35
36
|
|
|
36
37
|
|
|
38
|
+
def load_extra_vars(loader):
|
|
39
|
+
"""
|
|
40
|
+
Overriding Ansible function using version before slight var loading optimization
|
|
41
|
+
in order to avoid caching issues https://github.com/ansible/ansible/pull/78835/files
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
extra_vars = {}
|
|
45
|
+
for extra_vars_opt in context.CLIARGS.get('extra_vars', tuple()):
|
|
46
|
+
data = None
|
|
47
|
+
extra_vars_opt = to_text(extra_vars_opt, errors='surrogate_or_strict')
|
|
48
|
+
if extra_vars_opt is None or not extra_vars_opt:
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
if extra_vars_opt.startswith(u"@"):
|
|
52
|
+
# Argument is a YAML file (JSON is a subset of YAML)
|
|
53
|
+
data = loader.load_from_file(extra_vars_opt[1:])
|
|
54
|
+
elif extra_vars_opt[0] in [u'/', u'.']:
|
|
55
|
+
raise AnsibleOptionsError("Please prepend extra_vars filename '%s' with '@'" % extra_vars_opt)
|
|
56
|
+
elif extra_vars_opt[0] in [u'[', u'{']:
|
|
57
|
+
# Arguments as YAML
|
|
58
|
+
data = loader.load(extra_vars_opt)
|
|
59
|
+
else:
|
|
60
|
+
# Arguments as Key-value
|
|
61
|
+
data = parse_kv(extra_vars_opt)
|
|
62
|
+
|
|
63
|
+
if isinstance(data, MutableMapping):
|
|
64
|
+
extra_vars = combine_vars(extra_vars, data)
|
|
65
|
+
else:
|
|
66
|
+
raise AnsibleOptionsError("Invalid extra vars data supplied. '%s' could not be made into a dictionary" % extra_vars_opt)
|
|
67
|
+
return extra_vars
|
|
68
|
+
|
|
69
|
+
|
|
37
70
|
class ClusterConfig(object):
|
|
38
71
|
def __init__(self, cluster_config_generator,
|
|
39
72
|
ops_config, cluster_config_path):
|
|
@@ -119,7 +152,6 @@ class JinjaConfigGenerator(object):
|
|
|
119
152
|
context_cliargs['extra_vars'] = tuple(extra_vars)
|
|
120
153
|
|
|
121
154
|
context.CLIARGS = ImmutableDict(context_cliargs)
|
|
122
|
-
setattr(load_extra_vars, 'extra_vars', {})
|
|
123
155
|
variable_manager._extra_vars = load_extra_vars(
|
|
124
156
|
loader=data_loader)
|
|
125
157
|
|
|
@@ -159,7 +191,6 @@ class ClusterConfigGenerator(object):
|
|
|
159
191
|
context_cliargs['extra_vars'] = tuple(extra_vars)
|
|
160
192
|
|
|
161
193
|
context.CLIARGS = ImmutableDict(context_cliargs)
|
|
162
|
-
setattr(load_extra_vars, 'extra_vars', {})
|
|
163
194
|
variable_manager._extra_vars = load_extra_vars(
|
|
164
195
|
loader=data_loader)
|
|
165
196
|
|
ops/cli/inventory.py
CHANGED
|
@@ -53,7 +53,7 @@ class InventoryRunner(object):
|
|
|
53
53
|
group_names = [group.name for group in host.get_groups()]
|
|
54
54
|
group_names = sorted(group_names)
|
|
55
55
|
group_string = ", ".join(group_names)
|
|
56
|
-
host_id = host.vars.get('
|
|
56
|
+
host_id = host.vars.get('ec2_InstanceId', '')
|
|
57
57
|
if host_id != '':
|
|
58
58
|
name_and_id = "%s -- %s" % (stringc(host.name,
|
|
59
59
|
'blue'), stringc(host_id, 'blue'))
|
ops/cli/ssh.py
CHANGED
|
@@ -92,6 +92,12 @@ class SshParserConfig(SubParserConfig):
|
|
|
92
92
|
help='When using Shell Control Box (SCB) and creating a proxy,'
|
|
93
93
|
'a random port is generated, which will be used in the ssh config '
|
|
94
94
|
'for all playbook, run and sync operations')
|
|
95
|
+
parser.add_argument(
|
|
96
|
+
'--ssh-dest-user',
|
|
97
|
+
type=str,
|
|
98
|
+
dest='ssh_dest_user',
|
|
99
|
+
help='SSH User for the destination host, different from the bastion or SCB user. '
|
|
100
|
+
'Useful when LDAP is not working on the destination host.')
|
|
95
101
|
|
|
96
102
|
def get_help(self):
|
|
97
103
|
return 'SSH or create an SSH tunnel to a server in the cluster'
|
|
@@ -260,6 +266,12 @@ class SshRunner(object):
|
|
|
260
266
|
ssh_config = args.ssh_config or self.ops_config.get(
|
|
261
267
|
'ssh.config') or self.ansible_inventory.get_ssh_config()
|
|
262
268
|
|
|
269
|
+
ssh_host_bastion, ssh_host_dest = None, None
|
|
270
|
+
if args.ssh_dest_user:
|
|
271
|
+
ssh_host_parts = ssh_host.split('--')
|
|
272
|
+
ssh_host_bastion = ssh_host_parts[0]
|
|
273
|
+
ssh_host_dest = ssh_host_parts[1] if len(ssh_host_parts) > 1 else None
|
|
274
|
+
|
|
263
275
|
scb_ssh_host = None
|
|
264
276
|
if scb_enabled:
|
|
265
277
|
# scb->bastion->host vs scb->bastion
|
|
@@ -280,13 +292,19 @@ class SshRunner(object):
|
|
|
280
292
|
else:
|
|
281
293
|
if scb_enabled:
|
|
282
294
|
command = f"ssh -F {ssh_config} {ssh_user}@{scb_ssh_host}"
|
|
295
|
+
if args.ssh_dest_user and ssh_host_dest:
|
|
296
|
+
command = (f"ssh -F {ssh_config} -t {ssh_user}@{ssh_host_bastion}@{scb_host} "
|
|
297
|
+
f"ssh {args.ssh_dest_user}@{ssh_host_dest}")
|
|
283
298
|
else:
|
|
284
299
|
command = f"ssh -F {ssh_config} {ssh_host}"
|
|
300
|
+
if args.ssh_dest_user and ssh_host_dest:
|
|
301
|
+
command = (f"ssh -F {ssh_config} -t {ssh_user}@{ssh_host_bastion} "
|
|
302
|
+
f"ssh {args.ssh_dest_user}@{ssh_host_dest}")
|
|
285
303
|
|
|
286
304
|
if args.proxy:
|
|
287
305
|
if scb_enabled:
|
|
288
306
|
proxy_port = args.local or SshConfigGenerator.generate_ssh_scb_proxy_port(
|
|
289
|
-
self.ansible_inventory.generated_path.
|
|
307
|
+
self.ansible_inventory.generated_path.removesuffix("/inventory"),
|
|
290
308
|
args.auto_scb_port,
|
|
291
309
|
scb_proxy_port
|
|
292
310
|
)
|
ops/inventory/ec2inventory.py
CHANGED
|
@@ -11,26 +11,24 @@
|
|
|
11
11
|
import json
|
|
12
12
|
import re
|
|
13
13
|
import sys
|
|
14
|
-
import os
|
|
15
14
|
|
|
16
|
-
import
|
|
17
|
-
from
|
|
18
|
-
from boto.pyami.config import Config
|
|
19
|
-
|
|
20
|
-
from six import iteritems, string_types, integer_types
|
|
15
|
+
import boto3
|
|
16
|
+
from botocore.exceptions import NoRegionError, NoCredentialsError, PartialCredentialsError
|
|
21
17
|
|
|
22
18
|
|
|
23
19
|
class Ec2Inventory(object):
|
|
24
|
-
|
|
20
|
+
@staticmethod
|
|
21
|
+
def _empty_inventory():
|
|
25
22
|
return {"_meta": {"hostvars": {}}}
|
|
26
23
|
|
|
27
|
-
def __init__(self, boto_profile, regions, filters=
|
|
24
|
+
def __init__(self, boto_profile, regions, filters=None, bastion_filters=None):
|
|
28
25
|
|
|
29
|
-
self.filters = filters
|
|
26
|
+
self.filters = filters or []
|
|
30
27
|
self.regions = regions.split(',')
|
|
31
28
|
self.boto_profile = boto_profile
|
|
32
|
-
self.bastion_filters = bastion_filters
|
|
29
|
+
self.bastion_filters = bastion_filters or []
|
|
33
30
|
self.group_callbacks = []
|
|
31
|
+
self.boto3_session = self.create_boto3_session(boto_profile)
|
|
34
32
|
|
|
35
33
|
# Inventory grouped by instance IDs, tags, security groups, regions,
|
|
36
34
|
# and availability zones
|
|
@@ -39,6 +37,25 @@ class Ec2Inventory(object):
|
|
|
39
37
|
# Index of hostname (address) to instance ID
|
|
40
38
|
self.index = {}
|
|
41
39
|
|
|
40
|
+
def create_boto3_session(self, profile_name):
|
|
41
|
+
try:
|
|
42
|
+
# Use the profile to create a session
|
|
43
|
+
session = boto3.Session(profile_name=profile_name)
|
|
44
|
+
|
|
45
|
+
# Verify region
|
|
46
|
+
if not self.regions:
|
|
47
|
+
if not session.region_name:
|
|
48
|
+
raise NoRegionError
|
|
49
|
+
self.regions = [session.region_name]
|
|
50
|
+
|
|
51
|
+
except NoRegionError:
|
|
52
|
+
sys.exit(f"Region not specified and could not be determined for profile: {profile_name}")
|
|
53
|
+
except (NoCredentialsError, PartialCredentialsError):
|
|
54
|
+
sys.exit(f"Credentials not found or incomplete for profile: {profile_name}")
|
|
55
|
+
except Exception as e:
|
|
56
|
+
sys.exit(f"An error occurred: {str(e)}")
|
|
57
|
+
return session
|
|
58
|
+
|
|
42
59
|
def get_as_json(self):
|
|
43
60
|
self.do_api_calls_update_cache()
|
|
44
61
|
return self.json_format_dict(self.inventory, True)
|
|
@@ -55,20 +72,20 @@ class Ec2Inventory(object):
|
|
|
55
72
|
def group(self, *args):
|
|
56
73
|
self.group_callbacks.extend(args)
|
|
57
74
|
|
|
58
|
-
def find_bastion_box(self,
|
|
75
|
+
def find_bastion_box(self, ec2_client):
|
|
59
76
|
"""
|
|
60
77
|
Find ips for the bastion box
|
|
61
78
|
"""
|
|
62
79
|
|
|
63
|
-
if not self.bastion_filters
|
|
80
|
+
if not self.bastion_filters:
|
|
64
81
|
return
|
|
65
82
|
|
|
66
|
-
self.bastion_filters
|
|
83
|
+
self.bastion_filters.append({'Name': 'instance-state-name', 'Values': ['running']})
|
|
67
84
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
for instance in reservation
|
|
71
|
-
return instance
|
|
85
|
+
reservations = ec2_client.describe_instances(Filters=self.bastion_filters)['Reservations']
|
|
86
|
+
for reservation in reservations:
|
|
87
|
+
for instance in reservation['Instances']:
|
|
88
|
+
return instance['PublicIpAddress']
|
|
72
89
|
|
|
73
90
|
def do_api_calls_update_cache(self):
|
|
74
91
|
""" Do API calls to each region, and save data in cache files """
|
|
@@ -80,92 +97,67 @@ class Ec2Inventory(object):
|
|
|
80
97
|
"""Makes an AWS EC2 API call to the list of instances in a particular
|
|
81
98
|
region
|
|
82
99
|
"""
|
|
100
|
+
ec2_client = self.boto3_session.client('ec2', region_name=region)
|
|
83
101
|
|
|
84
|
-
|
|
85
|
-
cfg = Config()
|
|
86
|
-
cfg.load_credential_file(os.path.expanduser("~/.aws/credentials"))
|
|
87
|
-
cfg.load_credential_file(os.path.expanduser("~/.aws/config"))
|
|
88
|
-
session_token = cfg.get(self.boto_profile, "aws_session_token")
|
|
89
|
-
|
|
90
|
-
conn = ec2.connect_to_region(
|
|
91
|
-
region,
|
|
92
|
-
security_token=session_token,
|
|
93
|
-
profile_name=self.boto_profile)
|
|
94
|
-
|
|
95
|
-
# connect_to_region will fail "silently" by returning None if the
|
|
96
|
-
# region name is wrong or not supported
|
|
97
|
-
if conn is None:
|
|
98
|
-
sys.exit(
|
|
99
|
-
"region name: {} likely not supported, or AWS is down. "
|
|
100
|
-
"connection to region failed.".format(region))
|
|
102
|
+
reservations = ec2_client.describe_instances(Filters=self.filters)['Reservations']
|
|
101
103
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
instances = []
|
|
107
|
-
for reservation in reservations:
|
|
108
|
-
instances.extend(reservation.instances)
|
|
109
|
-
|
|
110
|
-
# sort the instance based on name and index, in this order
|
|
111
|
-
def sort_key(instance):
|
|
112
|
-
name = instance.tags.get('Name', '')
|
|
113
|
-
return "{}-{}".format(name, instance.id)
|
|
114
|
-
|
|
115
|
-
for instance in sorted(instances, key=sort_key):
|
|
116
|
-
self.add_instance(bastion_ip, instance, region)
|
|
104
|
+
bastion_ip = self.find_bastion_box(ec2_client)
|
|
105
|
+
instances = []
|
|
106
|
+
for reservation in reservations:
|
|
107
|
+
instances.extend(reservation['Instances'])
|
|
117
108
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
109
|
+
# sort the instance based on name and index, in this order
|
|
110
|
+
def sort_key(instance):
|
|
111
|
+
name = next((tag['Value'] for tag in instance.get('Tags', [])
|
|
112
|
+
if tag['Key'] == 'Name'), '')
|
|
113
|
+
return "{}-{}".format(name, instance['InstanceId'])
|
|
121
114
|
|
|
122
|
-
|
|
123
|
-
|
|
115
|
+
for instance in sorted(instances, key=sort_key):
|
|
116
|
+
self.add_instance(bastion_ip, instance, region)
|
|
124
117
|
|
|
125
118
|
def get_instance(self, region, instance_id):
|
|
126
119
|
""" Gets details about a specific instance """
|
|
127
|
-
|
|
128
|
-
|
|
120
|
+
ec2_client = self.boto3_session.client('ec2', region_name=region)
|
|
129
121
|
# connect_to_region will fail "silently" by returning None if the
|
|
130
122
|
# region name is wrong or not supported
|
|
131
|
-
if
|
|
123
|
+
if ec2_client is None:
|
|
132
124
|
sys.exit(
|
|
133
125
|
"region name: %s likely not supported, or AWS is down. "
|
|
134
126
|
"connection to region failed." % region
|
|
135
127
|
)
|
|
136
128
|
|
|
137
|
-
reservations =
|
|
129
|
+
reservations = ec2_client.describe_instances(InstanceIds=[instance_id])['Reservations']
|
|
138
130
|
for reservation in reservations:
|
|
139
|
-
for instance in reservation
|
|
131
|
+
for instance in reservation['Instances']:
|
|
140
132
|
return instance
|
|
141
133
|
|
|
142
134
|
def add_instance(self, bastion_ip, instance, region):
|
|
143
135
|
"""
|
|
144
|
-
:type instance:
|
|
136
|
+
:type instance: dict
|
|
145
137
|
"""
|
|
146
138
|
|
|
147
139
|
# Only want running instances unless all_instances is True
|
|
148
|
-
if instance
|
|
140
|
+
if instance['State']['Name'] != 'running':
|
|
149
141
|
return
|
|
150
142
|
|
|
151
143
|
# Use the instance name instead of the public ip
|
|
152
|
-
dest = instance.
|
|
144
|
+
dest = next((tag['Value'] for tag in instance.get('Tags', []) if tag['Key'] == 'Name'), instance.get('PublicIpAddress'))
|
|
153
145
|
if not dest:
|
|
154
146
|
return
|
|
155
147
|
|
|
156
|
-
if bastion_ip and bastion_ip != instance.
|
|
157
|
-
ansible_ssh_host = bastion_ip + "--" + instance.
|
|
158
|
-
elif instance.
|
|
159
|
-
ansible_ssh_host = instance.
|
|
148
|
+
if bastion_ip and bastion_ip != instance.get('PublicIpAddress'):
|
|
149
|
+
ansible_ssh_host = bastion_ip + "--" + instance.get('PrivateIpAddress')
|
|
150
|
+
elif instance.get('PublicIpAddress'):
|
|
151
|
+
ansible_ssh_host = instance.get('PublicIpAddress')
|
|
160
152
|
else:
|
|
161
|
-
ansible_ssh_host = instance.
|
|
153
|
+
ansible_ssh_host = instance.get('PrivateIpAddress')
|
|
162
154
|
|
|
163
155
|
# Add to index and append the instance id afterwards if it's already
|
|
164
156
|
# there
|
|
165
157
|
if dest in self.index:
|
|
166
|
-
dest = dest + "-" + instance.
|
|
158
|
+
dest = dest + "-" + instance['InstanceId'].replace("i-", "")
|
|
167
159
|
|
|
168
|
-
self.index[dest] = [region, instance
|
|
160
|
+
self.index[dest] = [region, instance['InstanceId']]
|
|
169
161
|
|
|
170
162
|
# group with dynamic groups
|
|
171
163
|
for grouping in set(self.group_callbacks):
|
|
@@ -175,9 +167,9 @@ class Ec2Inventory(object):
|
|
|
175
167
|
self.push(self.inventory, group, dest)
|
|
176
168
|
|
|
177
169
|
# Group by all tags
|
|
178
|
-
for tag in instance.
|
|
179
|
-
if tag:
|
|
180
|
-
self.push(self.inventory, tag, dest)
|
|
170
|
+
for tag in instance.get('Tags', []):
|
|
171
|
+
if tag['Value']:
|
|
172
|
+
self.push(self.inventory, tag['Value'], dest)
|
|
181
173
|
|
|
182
174
|
# Inventory: Group by region
|
|
183
175
|
self.push(self.inventory, region, dest)
|
|
@@ -186,56 +178,39 @@ class Ec2Inventory(object):
|
|
|
186
178
|
self.push(self.inventory, ansible_ssh_host, dest)
|
|
187
179
|
|
|
188
180
|
# Inventory: Group by availability zone
|
|
189
|
-
self.push(self.inventory, instance
|
|
181
|
+
self.push(self.inventory, instance['Placement']['AvailabilityZone'], dest)
|
|
190
182
|
|
|
191
|
-
self.inventory["_meta"]["hostvars"][dest] = self.get_host_info_dict_from_instance(
|
|
192
|
-
instance)
|
|
183
|
+
self.inventory["_meta"]["hostvars"][dest] = self.get_host_info_dict_from_instance(instance)
|
|
193
184
|
self.inventory["_meta"]["hostvars"][dest]['ansible_ssh_host'] = ansible_ssh_host
|
|
194
185
|
|
|
195
186
|
def get_host_info_dict_from_instance(self, instance):
|
|
196
187
|
instance_vars = {}
|
|
197
|
-
for key in
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
instance_vars[
|
|
207
|
-
elif key == 'ec2__previous_state':
|
|
208
|
-
instance_vars['ec2_previous_state'] = instance.previous_state or ''
|
|
209
|
-
instance_vars['ec2_previous_state_code'] = instance.previous_state_code
|
|
210
|
-
elif type(value) in integer_types or isinstance(value, bool):
|
|
211
|
-
instance_vars[key] = value
|
|
212
|
-
elif type(value) in string_types:
|
|
213
|
-
instance_vars[key] = value.strip()
|
|
188
|
+
for key, value in instance.items():
|
|
189
|
+
safe_key = self.to_safe('ec2_' + key)
|
|
190
|
+
|
|
191
|
+
if key == 'State':
|
|
192
|
+
instance_vars['ec2_state'] = value['Name']
|
|
193
|
+
instance_vars['ec2_state_code'] = value['Code']
|
|
194
|
+
elif isinstance(value, (int, bool)):
|
|
195
|
+
instance_vars[safe_key] = value
|
|
196
|
+
elif isinstance(value, str):
|
|
197
|
+
instance_vars[safe_key] = value.strip()
|
|
214
198
|
elif value is None:
|
|
215
|
-
instance_vars[
|
|
216
|
-
elif key == '
|
|
217
|
-
instance_vars[
|
|
218
|
-
elif key == '
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
group_ids = []
|
|
226
|
-
group_names = []
|
|
227
|
-
for group in value:
|
|
228
|
-
group_ids.append(group.id)
|
|
229
|
-
group_names.append(group.name)
|
|
199
|
+
instance_vars[safe_key] = ''
|
|
200
|
+
elif key == 'Placement':
|
|
201
|
+
instance_vars['ec2_placement'] = value['AvailabilityZone']
|
|
202
|
+
elif key == 'Tags':
|
|
203
|
+
for tag in value:
|
|
204
|
+
tag_key = self.to_safe('ec2_tag_' + tag['Key'])
|
|
205
|
+
instance_vars[tag_key] = tag['Value']
|
|
206
|
+
elif key == 'SecurityGroups':
|
|
207
|
+
group_ids = [group['GroupId'] for group in value]
|
|
208
|
+
group_names = [group['GroupName'] for group in value]
|
|
230
209
|
instance_vars["ec2_security_group_ids"] = ','.join(group_ids)
|
|
231
|
-
instance_vars["ec2_security_group_names"] = ','.join(
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
instance_vars['private_ip'] = instance_vars.get(
|
|
236
|
-
'ec2_private_ip_address', '')
|
|
237
|
-
instance_vars['private_ip_address'] = instance_vars.get(
|
|
238
|
-
'ec2_private_ip_address', '')
|
|
210
|
+
instance_vars["ec2_security_group_names"] = ','.join(group_names)
|
|
211
|
+
|
|
212
|
+
instance_vars['private_ip'] = instance.get('PrivateIpAddress', '')
|
|
213
|
+
instance_vars['private_ip_address'] = instance.get('PrivateIpAddress', '')
|
|
239
214
|
return instance_vars
|
|
240
215
|
|
|
241
216
|
def get_host_info(self):
|
ops/inventory/plugin/cns.py
CHANGED
|
@@ -27,13 +27,13 @@ def cns(args):
|
|
|
27
27
|
region=region,
|
|
28
28
|
boto_profile=profile,
|
|
29
29
|
cache=args.get('cache', 3600 * 24),
|
|
30
|
-
filters=
|
|
31
|
-
'tag:cluster': cns_cluster
|
|
32
|
-
|
|
33
|
-
bastion=
|
|
34
|
-
'tag:cluster': cns_cluster,
|
|
35
|
-
'tag:role': 'bastion'
|
|
36
|
-
|
|
30
|
+
filters=[
|
|
31
|
+
{'Name': 'tag:cluster', 'Values': [cns_cluster]}
|
|
32
|
+
],
|
|
33
|
+
bastion=[
|
|
34
|
+
{'Name': 'tag:cluster', 'Values': [cns_cluster]},
|
|
35
|
+
{'Name': 'tag:role', 'Values': ['bastion']}
|
|
36
|
+
]
|
|
37
37
|
))
|
|
38
38
|
|
|
39
39
|
merge_inventories(result, json.loads(jsn))
|
ops/inventory/plugin/ec2.py
CHANGED
|
@@ -12,15 +12,17 @@ from ops.inventory.ec2inventory import Ec2Inventory
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
def ec2(args):
|
|
15
|
-
filters = args.get('filters',
|
|
16
|
-
bastion_filters = args.get('bastion',
|
|
15
|
+
filters = args.get('filters', [])
|
|
16
|
+
bastion_filters = args.get('bastion', [])
|
|
17
17
|
|
|
18
18
|
if args.get('cluster') and not args.get('filters'):
|
|
19
|
-
filters['tag:cluster'
|
|
19
|
+
filters = [{'Name': 'tag:cluster', 'Values': [args.get('cluster')]}]
|
|
20
20
|
|
|
21
21
|
if args.get('cluster') and not args.get('bastion'):
|
|
22
|
-
bastion_filters
|
|
23
|
-
|
|
22
|
+
bastion_filters = [
|
|
23
|
+
{'Name': 'tag:cluster', 'Values': [args.get('cluster')]},
|
|
24
|
+
{'Name': 'tag:role', 'Values': ['bastion']}
|
|
25
|
+
]
|
|
24
26
|
|
|
25
27
|
return Ec2Inventory(boto_profile=args['boto_profile'],
|
|
26
28
|
regions=args['region'],
|
ops/inventory/sshconfig.py
CHANGED
|
@@ -90,7 +90,7 @@ class SshConfigGenerator(object):
|
|
|
90
90
|
ssh_config_content = ssh_config_template.format(
|
|
91
91
|
scb_proxy_port=scb_proxy_port
|
|
92
92
|
)
|
|
93
|
-
ssh_config_path = ssh_config_tpl_path.
|
|
93
|
+
ssh_config_path = ssh_config_tpl_path.removesuffix("_tpl")
|
|
94
94
|
with open(ssh_config_path, 'w') as f:
|
|
95
95
|
f.write(ssh_config_content)
|
|
96
96
|
os.fchmod(f.fileno(), 0o644)
|
ops/simplevault.py
CHANGED
|
@@ -19,11 +19,12 @@ Very simple secrets management that can be used to
|
|
|
19
19
|
(ex: generate it only if it's not already there)
|
|
20
20
|
- it will also attempt login, if it will be required
|
|
21
21
|
'''
|
|
22
|
+
|
|
22
23
|
import os
|
|
23
24
|
import hvac
|
|
24
25
|
import getpass
|
|
25
26
|
from .cli import display
|
|
26
|
-
from six import iteritems
|
|
27
|
+
from six import iteritems, string_types
|
|
27
28
|
|
|
28
29
|
MAX_LDAP_ATTEMPTS = 3
|
|
29
30
|
|
|
@@ -35,30 +36,6 @@ class SimpleVault(object):
|
|
|
35
36
|
def __init__(
|
|
36
37
|
self, vault_user=None, vault_addr=None, vault_token=None, namespace=None,
|
|
37
38
|
mount_point=None, persistent_session=True, auto_prompt=True):
|
|
38
|
-
def try_reading_token_file():
|
|
39
|
-
ret = None
|
|
40
|
-
try:
|
|
41
|
-
ret = open(
|
|
42
|
-
os.path.expanduser('~/.vault-token'),
|
|
43
|
-
"r").read().strip()
|
|
44
|
-
except Exception:
|
|
45
|
-
ret = None
|
|
46
|
-
pass
|
|
47
|
-
return ret
|
|
48
|
-
|
|
49
|
-
def write_token(token=None):
|
|
50
|
-
try:
|
|
51
|
-
if token:
|
|
52
|
-
open(
|
|
53
|
-
os.path.expanduser('~/.vault-token'),
|
|
54
|
-
"w").write(
|
|
55
|
-
token.strip())
|
|
56
|
-
except Exception:
|
|
57
|
-
display(
|
|
58
|
-
"Warning: could not persist token to ~/.vault-token",
|
|
59
|
-
stderr=False,
|
|
60
|
-
color='yellow')
|
|
61
|
-
pass
|
|
62
39
|
|
|
63
40
|
self.vault_addr = vault_addr or os.getenv(
|
|
64
41
|
'VAULT_ADDR', None) or "http://localhost:8200"
|
|
@@ -66,7 +43,7 @@ class SimpleVault(object):
|
|
|
66
43
|
# How often we will create infrastructures
|
|
67
44
|
# with vault running on the provisioner's machine ?
|
|
68
45
|
self.vault_token = vault_token or os.getenv(
|
|
69
|
-
'VAULT_TOKEN', None) or try_reading_token_file()
|
|
46
|
+
'VAULT_TOKEN', None) or self.try_reading_token_file()
|
|
70
47
|
self.vault_user = vault_user or os.getenv(
|
|
71
48
|
'VAULT_USER', None) or getpass.getuser()
|
|
72
49
|
self.mount_point = mount_point
|
|
@@ -82,34 +59,127 @@ class SimpleVault(object):
|
|
|
82
59
|
namespace=self.namespace,
|
|
83
60
|
token=self.vault_token)
|
|
84
61
|
|
|
62
|
+
auth_methods = {
|
|
63
|
+
'ldap': self.auth_with_ldap,
|
|
64
|
+
'okta': self.auth_with_okta
|
|
65
|
+
}
|
|
66
|
+
auth_method = os.getenv('OPS_VAULT_AUTH_METHOD', 'okta')
|
|
67
|
+
|
|
85
68
|
while not self.vault_conn.is_authenticated() and auto_prompt:
|
|
86
69
|
display("VAULT-LIB: Not authenticated to vault '%s'" %
|
|
87
70
|
self.vault_addr, stderr=True, color='red')
|
|
88
|
-
display("Note: the default
|
|
71
|
+
display("Note: the default Vault username (%s) can be overwritten with VAULT_USER"
|
|
89
72
|
% self.vault_user, stderr=True,
|
|
90
73
|
color='yellow')
|
|
91
74
|
display(
|
|
92
75
|
" or to pass a token directly use VAULT_TOKEN",
|
|
93
76
|
stderr=True,
|
|
94
77
|
color='yellow')
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
78
|
+
|
|
79
|
+
auth_method_func = auth_methods.get(auth_method)
|
|
80
|
+
if not auth_method_func:
|
|
81
|
+
raise ValueError(f"Unsupported authentication method: {auth_method}")
|
|
82
|
+
auth_method_func()
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
def try_reading_token_file():
|
|
86
|
+
ret = None
|
|
87
|
+
try:
|
|
88
|
+
ret = open(
|
|
89
|
+
os.path.expanduser('~/.vault-token'),
|
|
90
|
+
"r").read().strip()
|
|
91
|
+
except Exception:
|
|
92
|
+
ret = None
|
|
93
|
+
pass
|
|
94
|
+
return ret
|
|
95
|
+
|
|
96
|
+
@staticmethod
|
|
97
|
+
def write_token(token=None):
|
|
98
|
+
try:
|
|
99
|
+
if token:
|
|
100
|
+
open(
|
|
101
|
+
os.path.expanduser('~/.vault-token'),
|
|
102
|
+
"w").write(
|
|
103
|
+
token.strip())
|
|
104
|
+
except Exception:
|
|
105
|
+
display(
|
|
106
|
+
"Warning: could not persist token to ~/.vault-token",
|
|
107
|
+
stderr=False,
|
|
108
|
+
color='yellow')
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
def auth_with_ldap(self):
|
|
112
|
+
# LDAP authentication logic
|
|
113
|
+
try:
|
|
114
|
+
self.ldap_attempts += 1
|
|
115
|
+
ldap_password = getpass.getpass(
|
|
116
|
+
prompt='LDAP password for %s for server %s: ' %
|
|
117
|
+
(self.vault_user, self.vault_addr))
|
|
118
|
+
auth_response = self.vault_conn.auth.ldap.login(
|
|
119
|
+
username=self.vault_user, password=ldap_password)
|
|
120
|
+
self.vault_conn.is_authenticated()
|
|
121
|
+
self.vault_token = auth_response['auth']['client_token']
|
|
122
|
+
self.write_token(self.vault_token)
|
|
123
|
+
except Exception as e:
|
|
124
|
+
if self.ldap_attempts >= MAX_LDAP_ATTEMPTS:
|
|
125
|
+
display(
|
|
126
|
+
"FAILED authentication {} times".format(
|
|
127
|
+
self.ldap_attempts), color='red')
|
|
128
|
+
raise e
|
|
129
|
+
else:
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
def auth_with_okta(self):
|
|
133
|
+
try:
|
|
134
|
+
okta_password = getpass.getpass(
|
|
135
|
+
prompt=f"Okta password for {self.vault_user}: ")
|
|
136
|
+
|
|
137
|
+
display("Authenticating with Okta...", color='yellow')
|
|
138
|
+
auth_response = self.vault_conn.auth.okta.login(
|
|
139
|
+
username=self.vault_user, password=okta_password)
|
|
140
|
+
|
|
141
|
+
# Check the MFA requirement
|
|
142
|
+
auth_data = auth_response.get("auth", {})
|
|
143
|
+
mfa_requirement = auth_data.get("mfa_requirement")
|
|
144
|
+
|
|
145
|
+
mfa_request_id = mfa_requirement.get("mfa_request_id")
|
|
146
|
+
mfa_constraints = mfa_requirement.get("mfa_constraints", {})
|
|
147
|
+
|
|
148
|
+
okta_factors = mfa_constraints.get("okta", {}).get("any", [])
|
|
149
|
+
mfa_factor_id = okta_factors[0].get("id")
|
|
150
|
+
|
|
151
|
+
# Create the MFA validation payload
|
|
152
|
+
mfa_payload = {
|
|
153
|
+
"mfa_request_id": mfa_request_id,
|
|
154
|
+
"mfa_payload": {
|
|
155
|
+
mfa_factor_id: [] # No MFA factor specific data required
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
# Make the MFA validation request
|
|
160
|
+
display("Performing Okta MFA validation. Check notification...", color='yellow')
|
|
161
|
+
mfa_response = self.vault_conn.adapter.post(
|
|
162
|
+
"/v1/sys/mfa/validate",
|
|
163
|
+
json=mfa_payload,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Update token if provided after MFA
|
|
167
|
+
self.vault_token = self.vault_conn.adapter.get_login_token(mfa_response)
|
|
168
|
+
self.vault_conn.token = self.vault_token
|
|
169
|
+
self.write_token(self.vault_token)
|
|
170
|
+
if self.vault_conn.is_authenticated():
|
|
171
|
+
display("Okta MFA validation successful, token obtained.", color='green')
|
|
172
|
+
else:
|
|
173
|
+
display("Okta MFA validation failed."
|
|
174
|
+
"Please obtain a token manually (vault cli) and run ops again.",
|
|
175
|
+
stderr=True, color='red')
|
|
176
|
+
exit(1)
|
|
177
|
+
|
|
178
|
+
except Exception as e:
|
|
179
|
+
display(f"An error occurred during Okta authentication: {e}\n"
|
|
180
|
+
"Please obtain a token manually (vault cli) and run ops again.",
|
|
181
|
+
stderr=True, color='red')
|
|
182
|
+
exit(1)
|
|
113
183
|
|
|
114
184
|
def get(self, path, key='value', wrap_ttl=None,
|
|
115
185
|
default=None, fetch_all=False, raw=False):
|
|
@@ -149,7 +219,7 @@ class SimpleVault(object):
|
|
|
149
219
|
|
|
150
220
|
def put(self, path, value, lease=None, wrap_ttl=None):
|
|
151
221
|
payload = {}
|
|
152
|
-
if isinstance(value, (
|
|
222
|
+
if isinstance(value, (string_types, int, float, bool)):
|
|
153
223
|
payload['value'] = str(value)
|
|
154
224
|
elif isinstance(value, dict):
|
|
155
225
|
for k, v in iteritems(value):
|
|
@@ -34,8 +34,10 @@ class TerraformCommandGenerator(object):
|
|
|
34
34
|
current_terraform_version = self.check_terraform_version()
|
|
35
35
|
config = self.cluster_config
|
|
36
36
|
|
|
37
|
-
current_terraform_version_major = int(
|
|
38
|
-
|
|
37
|
+
current_terraform_version_major = int(current_terraform_version.removeprefix('v')
|
|
38
|
+
.split('.')[0])
|
|
39
|
+
current_terraform_version_minor = int(current_terraform_version
|
|
40
|
+
.split('.')[1])
|
|
39
41
|
if 'enable_consul_remote_state' in config['terraform']:
|
|
40
42
|
terraform_remote_state = config['terraform']['enable_consul_remote_state']
|
|
41
43
|
elif config['terraform'].get('state', {'type': None}).get('type') == 's3':
|
|
@@ -69,7 +71,9 @@ class TerraformCommandGenerator(object):
|
|
|
69
71
|
cluster=config['cluster'])
|
|
70
72
|
landscape = ''
|
|
71
73
|
|
|
72
|
-
if current_terraform_version_major
|
|
74
|
+
if (current_terraform_version_major == 0 and
|
|
75
|
+
current_terraform_version_minor >= 9 or
|
|
76
|
+
current_terraform_version_major > 0):
|
|
73
77
|
if args.force_copy:
|
|
74
78
|
terraform_init_command = 'terraform init -force-copy && '
|
|
75
79
|
else:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: ops-cli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: Ops - wrapper for Terraform, Ansible, and SSH for cloud automation
|
|
5
5
|
Home-page: https://github.com/adobe/ops-cli
|
|
6
6
|
Author: Adobe
|
|
@@ -23,146 +23,155 @@ Classifier: Topic :: Text Processing :: Markup :: HTML
|
|
|
23
23
|
Requires-Python: >=3.5
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
25
25
|
License-File: LICENSE
|
|
26
|
-
Requires-Dist: adal
|
|
27
|
-
Requires-Dist: ansible
|
|
28
|
-
Requires-Dist:
|
|
29
|
-
Requires-Dist:
|
|
30
|
-
Requires-Dist: azure
|
|
31
|
-
Requires-Dist: azure-
|
|
32
|
-
Requires-Dist: azure-
|
|
33
|
-
Requires-Dist: azure-
|
|
34
|
-
Requires-Dist: azure-
|
|
35
|
-
Requires-Dist: azure-
|
|
36
|
-
Requires-Dist: azure-
|
|
37
|
-
Requires-Dist: azure-
|
|
38
|
-
Requires-Dist: azure-
|
|
39
|
-
Requires-Dist: azure-
|
|
40
|
-
Requires-Dist: azure-
|
|
41
|
-
Requires-Dist: azure-
|
|
42
|
-
Requires-Dist: azure-mgmt
|
|
43
|
-
Requires-Dist: azure-mgmt-
|
|
44
|
-
Requires-Dist: azure-mgmt-
|
|
45
|
-
Requires-Dist: azure-mgmt-
|
|
46
|
-
Requires-Dist: azure-mgmt-
|
|
47
|
-
Requires-Dist: azure-mgmt-
|
|
48
|
-
Requires-Dist: azure-mgmt-
|
|
49
|
-
Requires-Dist: azure-mgmt-
|
|
50
|
-
Requires-Dist: azure-mgmt-
|
|
51
|
-
Requires-Dist: azure-mgmt-
|
|
52
|
-
Requires-Dist: azure-mgmt-
|
|
53
|
-
Requires-Dist: azure-mgmt-
|
|
54
|
-
Requires-Dist: azure-mgmt-
|
|
55
|
-
Requires-Dist: azure-mgmt-
|
|
56
|
-
Requires-Dist: azure-mgmt-
|
|
57
|
-
Requires-Dist: azure-mgmt-
|
|
58
|
-
Requires-Dist: azure-mgmt-
|
|
59
|
-
Requires-Dist: azure-mgmt-datalake-
|
|
60
|
-
Requires-Dist: azure-mgmt-
|
|
61
|
-
Requires-Dist: azure-mgmt-
|
|
62
|
-
Requires-Dist: azure-mgmt-
|
|
63
|
-
Requires-Dist: azure-mgmt-
|
|
64
|
-
Requires-Dist: azure-mgmt-
|
|
65
|
-
Requires-Dist: azure-mgmt-
|
|
66
|
-
Requires-Dist: azure-mgmt-
|
|
67
|
-
Requires-Dist: azure-mgmt-
|
|
68
|
-
Requires-Dist: azure-mgmt-
|
|
69
|
-
Requires-Dist: azure-mgmt-
|
|
70
|
-
Requires-Dist: azure-mgmt-
|
|
71
|
-
Requires-Dist: azure-mgmt-
|
|
72
|
-
Requires-Dist: azure-mgmt-
|
|
73
|
-
Requires-Dist: azure-mgmt-
|
|
74
|
-
Requires-Dist: azure-mgmt-
|
|
75
|
-
Requires-Dist: azure-mgmt-
|
|
76
|
-
Requires-Dist: azure-mgmt-
|
|
77
|
-
Requires-Dist: azure-mgmt-
|
|
78
|
-
Requires-Dist: azure-mgmt-
|
|
79
|
-
Requires-Dist: azure-mgmt-
|
|
80
|
-
Requires-Dist: azure-mgmt-
|
|
81
|
-
Requires-Dist: azure-mgmt-
|
|
82
|
-
Requires-Dist: azure-mgmt-
|
|
83
|
-
Requires-Dist: azure-mgmt-
|
|
84
|
-
Requires-Dist: azure-mgmt-
|
|
85
|
-
Requires-Dist: azure-mgmt-
|
|
86
|
-
Requires-Dist: azure-mgmt-
|
|
87
|
-
Requires-Dist: azure-mgmt-
|
|
88
|
-
Requires-Dist: azure-mgmt-
|
|
89
|
-
Requires-Dist: azure-mgmt-
|
|
90
|
-
Requires-Dist: azure-mgmt-
|
|
91
|
-
Requires-Dist: azure-mgmt-
|
|
92
|
-
Requires-Dist: azure-mgmt-
|
|
93
|
-
Requires-Dist: azure-mgmt-
|
|
94
|
-
Requires-Dist: azure-mgmt-
|
|
95
|
-
Requires-Dist: azure-mgmt-
|
|
96
|
-
Requires-Dist: azure-mgmt-
|
|
97
|
-
Requires-Dist: azure-mgmt-
|
|
98
|
-
Requires-Dist: azure-mgmt-
|
|
99
|
-
Requires-Dist: azure-mgmt-
|
|
100
|
-
Requires-Dist: azure-mgmt-
|
|
101
|
-
Requires-Dist: azure-mgmt-
|
|
102
|
-
Requires-Dist: azure-mgmt-
|
|
103
|
-
Requires-Dist: azure-
|
|
104
|
-
Requires-Dist: azure-
|
|
105
|
-
Requires-Dist: azure-
|
|
106
|
-
Requires-Dist: azure-
|
|
107
|
-
Requires-Dist: azure-
|
|
108
|
-
Requires-Dist: azure-
|
|
109
|
-
Requires-Dist: azure-storage-
|
|
110
|
-
Requires-Dist: azure-storage-
|
|
111
|
-
Requires-Dist:
|
|
112
|
-
Requires-Dist:
|
|
113
|
-
Requires-Dist:
|
|
114
|
-
Requires-Dist:
|
|
115
|
-
Requires-Dist:
|
|
116
|
-
Requires-Dist:
|
|
117
|
-
Requires-Dist:
|
|
118
|
-
Requires-Dist:
|
|
119
|
-
Requires-Dist:
|
|
120
|
-
Requires-Dist:
|
|
121
|
-
Requires-Dist:
|
|
122
|
-
Requires-Dist:
|
|
123
|
-
Requires-Dist:
|
|
124
|
-
Requires-Dist:
|
|
125
|
-
Requires-Dist:
|
|
126
|
-
Requires-Dist:
|
|
127
|
-
Requires-Dist:
|
|
128
|
-
Requires-Dist:
|
|
129
|
-
Requires-Dist:
|
|
130
|
-
Requires-Dist:
|
|
131
|
-
Requires-Dist:
|
|
132
|
-
Requires-Dist:
|
|
133
|
-
Requires-Dist:
|
|
134
|
-
Requires-Dist:
|
|
135
|
-
Requires-Dist:
|
|
136
|
-
Requires-Dist:
|
|
137
|
-
Requires-Dist:
|
|
138
|
-
Requires-Dist:
|
|
139
|
-
Requires-Dist:
|
|
140
|
-
Requires-Dist:
|
|
141
|
-
Requires-Dist:
|
|
142
|
-
Requires-Dist:
|
|
143
|
-
Requires-Dist:
|
|
144
|
-
Requires-Dist:
|
|
145
|
-
Requires-Dist:
|
|
146
|
-
Requires-Dist:
|
|
147
|
-
Requires-Dist:
|
|
148
|
-
Requires-Dist:
|
|
149
|
-
Requires-Dist:
|
|
150
|
-
Requires-Dist:
|
|
151
|
-
Requires-Dist:
|
|
152
|
-
Requires-Dist:
|
|
153
|
-
Requires-Dist:
|
|
154
|
-
Requires-Dist:
|
|
155
|
-
Requires-Dist:
|
|
156
|
-
Requires-Dist:
|
|
157
|
-
Requires-Dist:
|
|
158
|
-
Requires-Dist:
|
|
159
|
-
Requires-Dist:
|
|
160
|
-
Requires-Dist:
|
|
161
|
-
Requires-Dist:
|
|
162
|
-
Requires-Dist:
|
|
163
|
-
Requires-Dist:
|
|
164
|
-
Requires-Dist: websocket-client
|
|
165
|
-
|
|
26
|
+
Requires-Dist: adal==1.2.7
|
|
27
|
+
Requires-Dist: ansible==8.7.0; python_version >= "3.9"
|
|
28
|
+
Requires-Dist: ansible-core==2.15.13; python_version >= "3.9"
|
|
29
|
+
Requires-Dist: awscli==1.32.6; python_version >= "3.8"
|
|
30
|
+
Requires-Dist: azure==4.0.0
|
|
31
|
+
Requires-Dist: azure-applicationinsights==0.1.1
|
|
32
|
+
Requires-Dist: azure-batch==4.1.3
|
|
33
|
+
Requires-Dist: azure-common==1.1.28
|
|
34
|
+
Requires-Dist: azure-core==1.32.0; python_version >= "3.8"
|
|
35
|
+
Requires-Dist: azure-cosmosdb-nspkg==2.0.2
|
|
36
|
+
Requires-Dist: azure-cosmosdb-table==1.0.6
|
|
37
|
+
Requires-Dist: azure-datalake-store==0.0.53
|
|
38
|
+
Requires-Dist: azure-eventgrid==1.3.0
|
|
39
|
+
Requires-Dist: azure-graphrbac==0.40.0
|
|
40
|
+
Requires-Dist: azure-keyvault==1.1.0
|
|
41
|
+
Requires-Dist: azure-loganalytics==0.1.1
|
|
42
|
+
Requires-Dist: azure-mgmt==4.0.0
|
|
43
|
+
Requires-Dist: azure-mgmt-advisor==1.0.1
|
|
44
|
+
Requires-Dist: azure-mgmt-applicationinsights==0.1.1
|
|
45
|
+
Requires-Dist: azure-mgmt-authorization==0.50.0
|
|
46
|
+
Requires-Dist: azure-mgmt-batch==5.0.1
|
|
47
|
+
Requires-Dist: azure-mgmt-batchai==2.0.0
|
|
48
|
+
Requires-Dist: azure-mgmt-billing==0.2.0
|
|
49
|
+
Requires-Dist: azure-mgmt-cdn==3.1.0
|
|
50
|
+
Requires-Dist: azure-mgmt-cognitiveservices==3.0.0
|
|
51
|
+
Requires-Dist: azure-mgmt-commerce==1.0.1
|
|
52
|
+
Requires-Dist: azure-mgmt-compute==4.6.2
|
|
53
|
+
Requires-Dist: azure-mgmt-consumption==2.0.0
|
|
54
|
+
Requires-Dist: azure-mgmt-containerinstance==1.5.0
|
|
55
|
+
Requires-Dist: azure-mgmt-containerregistry==2.8.0
|
|
56
|
+
Requires-Dist: azure-mgmt-containerservice==4.4.0
|
|
57
|
+
Requires-Dist: azure-mgmt-cosmosdb==0.4.1
|
|
58
|
+
Requires-Dist: azure-mgmt-datafactory==0.6.0
|
|
59
|
+
Requires-Dist: azure-mgmt-datalake-analytics==0.6.0
|
|
60
|
+
Requires-Dist: azure-mgmt-datalake-nspkg==3.0.1
|
|
61
|
+
Requires-Dist: azure-mgmt-datalake-store==0.5.0
|
|
62
|
+
Requires-Dist: azure-mgmt-datamigration==1.0.0
|
|
63
|
+
Requires-Dist: azure-mgmt-devspaces==0.1.0
|
|
64
|
+
Requires-Dist: azure-mgmt-devtestlabs==2.2.0
|
|
65
|
+
Requires-Dist: azure-mgmt-dns==2.1.0
|
|
66
|
+
Requires-Dist: azure-mgmt-eventgrid==1.0.0
|
|
67
|
+
Requires-Dist: azure-mgmt-eventhub==2.6.0
|
|
68
|
+
Requires-Dist: azure-mgmt-hanaonazure==0.1.1
|
|
69
|
+
Requires-Dist: azure-mgmt-iotcentral==0.1.0
|
|
70
|
+
Requires-Dist: azure-mgmt-iothub==0.5.0
|
|
71
|
+
Requires-Dist: azure-mgmt-iothubprovisioningservices==0.2.0
|
|
72
|
+
Requires-Dist: azure-mgmt-keyvault==1.1.0
|
|
73
|
+
Requires-Dist: azure-mgmt-loganalytics==0.2.0
|
|
74
|
+
Requires-Dist: azure-mgmt-logic==3.0.0
|
|
75
|
+
Requires-Dist: azure-mgmt-machinelearningcompute==0.4.1
|
|
76
|
+
Requires-Dist: azure-mgmt-managementgroups==0.1.0
|
|
77
|
+
Requires-Dist: azure-mgmt-managementpartner==0.1.1
|
|
78
|
+
Requires-Dist: azure-mgmt-maps==0.1.0
|
|
79
|
+
Requires-Dist: azure-mgmt-marketplaceordering==0.1.0
|
|
80
|
+
Requires-Dist: azure-mgmt-media==1.0.1
|
|
81
|
+
Requires-Dist: azure-mgmt-monitor==0.5.2
|
|
82
|
+
Requires-Dist: azure-mgmt-msi==0.2.0
|
|
83
|
+
Requires-Dist: azure-mgmt-network==2.7.0
|
|
84
|
+
Requires-Dist: azure-mgmt-notificationhubs==2.1.0
|
|
85
|
+
Requires-Dist: azure-mgmt-nspkg==3.0.2
|
|
86
|
+
Requires-Dist: azure-mgmt-policyinsights==0.1.0
|
|
87
|
+
Requires-Dist: azure-mgmt-powerbiembedded==2.0.0
|
|
88
|
+
Requires-Dist: azure-mgmt-rdbms==1.9.0
|
|
89
|
+
Requires-Dist: azure-mgmt-recoveryservices==0.3.0
|
|
90
|
+
Requires-Dist: azure-mgmt-recoveryservicesbackup==0.3.0
|
|
91
|
+
Requires-Dist: azure-mgmt-redis==5.0.0
|
|
92
|
+
Requires-Dist: azure-mgmt-relay==0.1.0
|
|
93
|
+
Requires-Dist: azure-mgmt-reservations==0.2.1
|
|
94
|
+
Requires-Dist: azure-mgmt-resource==2.2.0
|
|
95
|
+
Requires-Dist: azure-mgmt-scheduler==2.0.0
|
|
96
|
+
Requires-Dist: azure-mgmt-search==2.1.0
|
|
97
|
+
Requires-Dist: azure-mgmt-servicebus==0.5.3
|
|
98
|
+
Requires-Dist: azure-mgmt-servicefabric==0.2.0
|
|
99
|
+
Requires-Dist: azure-mgmt-signalr==0.1.1
|
|
100
|
+
Requires-Dist: azure-mgmt-sql==0.9.1
|
|
101
|
+
Requires-Dist: azure-mgmt-storage==2.0.0
|
|
102
|
+
Requires-Dist: azure-mgmt-subscription==0.2.0
|
|
103
|
+
Requires-Dist: azure-mgmt-trafficmanager==0.50.0
|
|
104
|
+
Requires-Dist: azure-mgmt-web==0.35.0
|
|
105
|
+
Requires-Dist: azure-nspkg==3.0.2
|
|
106
|
+
Requires-Dist: azure-servicebus==0.21.1
|
|
107
|
+
Requires-Dist: azure-servicefabric==6.3.0.0
|
|
108
|
+
Requires-Dist: azure-servicemanagement-legacy==0.20.8
|
|
109
|
+
Requires-Dist: azure-storage-blob==1.5.0
|
|
110
|
+
Requires-Dist: azure-storage-common==1.4.2
|
|
111
|
+
Requires-Dist: azure-storage-file==1.4.0
|
|
112
|
+
Requires-Dist: azure-storage-queue==1.4.0
|
|
113
|
+
Requires-Dist: backports.functools-lru-cache==1.6.6; python_version >= "2.6"
|
|
114
|
+
Requires-Dist: boto3==1.34.6; python_version >= "3.8"
|
|
115
|
+
Requires-Dist: botocore==1.34.6; python_version >= "3.8"
|
|
116
|
+
Requires-Dist: cachetools==5.5.0; python_version >= "3.7"
|
|
117
|
+
Requires-Dist: certifi==2024.12.14; python_version >= "3.6"
|
|
118
|
+
Requires-Dist: cffi==1.17.1; python_version >= "3.8"
|
|
119
|
+
Requires-Dist: charset-normalizer==3.4.1; python_version >= "3.7"
|
|
120
|
+
Requires-Dist: colorama==0.4.4; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3, 3.4"
|
|
121
|
+
Requires-Dist: cryptography==44.0.0; python_version >= "3.7" and python_full_version not in "3.9.0, 3.9.1"
|
|
122
|
+
Requires-Dist: deepmerge==1.1.1
|
|
123
|
+
Requires-Dist: docutils==0.16; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3, 3.4"
|
|
124
|
+
Requires-Dist: gitdb==4.0.12; python_version >= "3.7"
|
|
125
|
+
Requires-Dist: gitpython==3.1.44; python_version >= "3.7"
|
|
126
|
+
Requires-Dist: google-auth==2.37.0; python_version >= "3.7"
|
|
127
|
+
Requires-Dist: hashmerge==0.2
|
|
128
|
+
Requires-Dist: himl==0.15.2; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3"
|
|
129
|
+
Requires-Dist: hvac==1.2.1; python_full_version >= "3.6.2" and python_full_version < "4.0.0"
|
|
130
|
+
Requires-Dist: idna==3.10; python_version >= "3.6"
|
|
131
|
+
Requires-Dist: inflection==0.5.1; python_version >= "3.5"
|
|
132
|
+
Requires-Dist: isodate==0.7.2; python_version >= "3.7"
|
|
133
|
+
Requires-Dist: jinja2==3.1.4; python_version >= "3.7"
|
|
134
|
+
Requires-Dist: jmespath==1.0.1; python_version >= "3.7"
|
|
135
|
+
Requires-Dist: kubernetes==26.1.0; python_version >= "3.6"
|
|
136
|
+
Requires-Dist: lru-cache==0.2.3
|
|
137
|
+
Requires-Dist: markupsafe==3.0.2; python_version >= "3.9"
|
|
138
|
+
Requires-Dist: msal==1.31.1; python_version >= "3.7"
|
|
139
|
+
Requires-Dist: msrest==0.7.1; python_version >= "3.6"
|
|
140
|
+
Requires-Dist: msrestazure==0.6.4
|
|
141
|
+
Requires-Dist: oauthlib==3.2.2; python_version >= "3.6"
|
|
142
|
+
Requires-Dist: packaging==24.2; python_version >= "3.8"
|
|
143
|
+
Requires-Dist: passgen==1.1.1
|
|
144
|
+
Requires-Dist: pathlib2==2.3.7.post1
|
|
145
|
+
Requires-Dist: pyasn1==0.6.1; python_version >= "3.8"
|
|
146
|
+
Requires-Dist: pyasn1-modules==0.4.1; python_version >= "3.8"
|
|
147
|
+
Requires-Dist: pycparser==2.22; python_version >= "3.8"
|
|
148
|
+
Requires-Dist: pyhcl==0.4.5
|
|
149
|
+
Requires-Dist: pyjwt[crypto]==2.10.1; python_version >= "3.9"
|
|
150
|
+
Requires-Dist: python-consul==1.1.0
|
|
151
|
+
Requires-Dist: python-dateutil==2.9.0.post0; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2"
|
|
152
|
+
Requires-Dist: pyyaml==6.0.1; python_version >= "3.6"
|
|
153
|
+
Requires-Dist: requests==2.32.3; python_version >= "3.8"
|
|
154
|
+
Requires-Dist: requests-oauthlib==2.0.0; python_version >= "3.4"
|
|
155
|
+
Requires-Dist: resolvelib==1.0.1
|
|
156
|
+
Requires-Dist: rsa==4.7.2; python_version >= "3.5" and python_version < "4"
|
|
157
|
+
Requires-Dist: s3transfer==0.10.4; python_version >= "3.8"
|
|
158
|
+
Requires-Dist: setuptools==75.8.0; python_version >= "3.9"
|
|
159
|
+
Requires-Dist: simpledi==0.4.1
|
|
160
|
+
Requires-Dist: six==1.17.0; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2"
|
|
161
|
+
Requires-Dist: smmap==5.0.2; python_version >= "3.7"
|
|
162
|
+
Requires-Dist: typing-extensions==4.12.2; python_version >= "3.8"
|
|
163
|
+
Requires-Dist: urllib3==2.0.7; python_version >= "3.7"
|
|
164
|
+
Requires-Dist: websocket-client==1.8.0; python_version >= "3.8"
|
|
165
|
+
Dynamic: author
|
|
166
|
+
Dynamic: author-email
|
|
167
|
+
Dynamic: classifier
|
|
168
|
+
Dynamic: description
|
|
169
|
+
Dynamic: description-content-type
|
|
170
|
+
Dynamic: home-page
|
|
171
|
+
Dynamic: license
|
|
172
|
+
Dynamic: requires-dist
|
|
173
|
+
Dynamic: requires-python
|
|
174
|
+
Dynamic: summary
|
|
166
175
|
|
|
167
176
|
# Ops CLI
|
|
168
177
|
[](https://github.com/adobe/ops-cli/actions/workflows/release.yml) [](https://github.com/adobe/ops-cli/pkgs/container/ops-cli) [](https://github.com/adobe/ops-cli/blob/master/LICENSE)
|
|
@@ -208,6 +217,7 @@ It can be used to add a layer of templating (using jinja2) on top of Terraform f
|
|
|
208
217
|
* [Terraform landscape](#terraform-landscape)
|
|
209
218
|
* [SSH](#ssh)
|
|
210
219
|
* [SSHPass](#sshpass)
|
|
220
|
+
* [Balabit SCB](#scb)
|
|
211
221
|
* [Play](#play)
|
|
212
222
|
* [Run command](#run-command)
|
|
213
223
|
* [Sync files](#sync-files)
|
|
@@ -316,7 +326,7 @@ workon ops
|
|
|
316
326
|
# uninstall previous `ops` version (if you have it)
|
|
317
327
|
pip uninstall ops --yes
|
|
318
328
|
|
|
319
|
-
# install ops-cli v2.
|
|
329
|
+
# install ops-cli v2.3.0 stable release
|
|
320
330
|
pip install --upgrade ops-cli
|
|
321
331
|
```
|
|
322
332
|
|
|
@@ -332,7 +342,7 @@ You can try out `ops-cli`, by using docker. The docker image has all required pr
|
|
|
332
342
|
|
|
333
343
|
To start out a container, running the latest `ops-cli` docker image run:
|
|
334
344
|
```sh
|
|
335
|
-
docker run -it ghcr.io/adobe/ops-cli:2.
|
|
345
|
+
docker run -it ghcr.io/adobe/ops-cli:2.3.0 bash
|
|
336
346
|
```
|
|
337
347
|
|
|
338
348
|
After the container has started, you can start using `ops-cli`:
|
|
@@ -858,6 +868,12 @@ Which will translate behind the scenes in :
|
|
|
858
868
|
This allows us to just refer in cluster files a secret that actually exists in vault and make sure we only generate it once - if it was already created by os or any other system, we will just use what is already there.
|
|
859
869
|
The reference is by means of fixed form jinja call added to the cluster file, which ends up interpreted later during the templating phase.
|
|
860
870
|
|
|
871
|
+
#### Vault auth
|
|
872
|
+
Ops checks if a valid vault token is present in `~/.vault-token` or in the environment variable `VAULT_TOKEN`. If not, it will try to authenticate using the method defined in `OPS_VAULT_AUTH_METHOD` environment variable.
|
|
873
|
+
The following methods are supported:
|
|
874
|
+
- `okta` - use the Vault Okta auth method (default)
|
|
875
|
+
- `ldap` - use the Vault ldap auth method
|
|
876
|
+
|
|
861
877
|
### Amazon Secrets Manager (SSM)
|
|
862
878
|
|
|
863
879
|
Amazon offers the possibility to use their [Secrets Manager](https://docs.aws.amazon.com/systems-manager/latest/userguide/what-is-systems-manager.html) in order to manage configuration data such as credentials, passwords and license keys.
|
|
@@ -4,7 +4,7 @@ ops/main.py,sha256=FvrpZHVawYsxBgJub9PuZP148HzWrYlnXuELNx5mBwI,6988
|
|
|
4
4
|
ops/opsconfig.py,sha256=kIiV2mU5yts644x2VTYUnUSJRVLofGIJMD4xwweaw0o,5847
|
|
5
5
|
ops/simpleconsul.py,sha256=4JWgUmw7RKn_Ti6gguf_bIyJnF7fpVBWkCXwcngbQVI,4089
|
|
6
6
|
ops/simplessm.py,sha256=BwD8P9EKagJmR4BR2aKAXaGLIren9Yljb1GxB8nzYbk,1703
|
|
7
|
-
ops/simplevault.py,sha256=
|
|
7
|
+
ops/simplevault.py,sha256=qVEA0L5UcZEFAVpY3CNQsMiyxHNyx8Fu7kEPiKbs8yE,12697
|
|
8
8
|
ops/ansible/__init__.py,sha256=n5YIjodX0NasK2cpc8kU88jZfLQF8j94F05V5k2vAhI,603
|
|
9
9
|
ops/ansible/callback_plugins/__init__.py,sha256=v9uW7YO2C7EC4MQWr_TMH_K-1ALd0uLl9vZ_h3xCQkg,596
|
|
10
10
|
ops/ansible/filter_plugins/__init__.py,sha256=v9uW7YO2C7EC4MQWr_TMH_K-1ALd0uLl9vZ_h3xCQkg,596
|
|
@@ -14,15 +14,15 @@ ops/ansible/vars_plugins/clusterconfig.py,sha256=qmIrTp1V0gLdbtBI5A0HMmICe8z0xUq
|
|
|
14
14
|
ops/ansible/vars_plugins/opsconfig.py,sha256=lfKCD7XRoDTW80QXEGcm3tV6ASnzoCGdxy5zpaimT4k,1615
|
|
15
15
|
ops/cli/__init__.py,sha256=LcPvjP6p7l8At8CIjyWvm0mhYzMGJqFv45AcTls1IhM,1376
|
|
16
16
|
ops/cli/aws.py,sha256=OsmpOVv1924_lvNESeElLgcEi1caYTnI88C7wzXie78,911
|
|
17
|
-
ops/cli/config.py,sha256
|
|
17
|
+
ops/cli/config.py,sha256=T0ZfW1bWdSr1dF4oqsvTjUtUiCr7RVm4_rFdezgGCqM,7629
|
|
18
18
|
ops/cli/config_generator.py,sha256=ARGp5kAgdMLqtrHyQb_dE9sEuM1bAnVddfWDxd_5OdA,1718
|
|
19
19
|
ops/cli/helmfile.py,sha256=2ipKyFMY5M87U1mXDQle_UsOXICdQ-0VX2f97TAg0I8,6354
|
|
20
|
-
ops/cli/inventory.py,sha256=
|
|
20
|
+
ops/cli/inventory.py,sha256=kSqNw978_P7OrFzLng5ipzuJBh6WqtvjvLDtiNwFQ8A,3109
|
|
21
21
|
ops/cli/packer.py,sha256=GAgi6uPWO0wv3uIEwl0O2sb4fQtpCbL1561nP8Y_Lpc,2626
|
|
22
22
|
ops/cli/parser.py,sha256=tzXvpu4GycyHSjERFrjQVEFZYVzOszLhDEjpkFYJ-oc,4377
|
|
23
23
|
ops/cli/playbook.py,sha256=rPaIfUUMSTgITxzrvkVyTpPJ2UfqYnk3WfjY3jDrHO0,5287
|
|
24
24
|
ops/cli/run.py,sha256=cwT2rQEBRip_OndkSQLFR7Q8D5BD6L5BbAq6nehKhco,3373
|
|
25
|
-
ops/cli/ssh.py,sha256=
|
|
25
|
+
ops/cli/ssh.py,sha256=AGDtNqLnbOjIbVxAnxfEJDpi5m1D0tOO3ml3uQ3bhgQ,14196
|
|
26
26
|
ops/cli/sync.py,sha256=8K3Dsh6cwv9gTat0KXvRemHmqpAWd46pHfS8wMS9Olk,5663
|
|
27
27
|
ops/cli/terraform.py,sha256=XeLXr_nFpdnsHdEfAFj_oiurvIhDQv-xE3L-NpMTcB0,12446
|
|
28
28
|
ops/data/ansible/ansible.cfg,sha256=LE0Ve37qwQnbM1AJfHX442yh6HMMzM1FPCTCUKZElV8,186
|
|
@@ -39,21 +39,21 @@ ops/inventory/SKMS.py,sha256=jv4nHTKbDmJBTNf3HgGyKSfAJ10akKdDxNoGOcPehfo,17616
|
|
|
39
39
|
ops/inventory/__init__.py,sha256=yvYO8z4aCdcOEYIjy27YL7fcbPqZAwQk9y_3YcNVAGU,643
|
|
40
40
|
ops/inventory/azurerm.py,sha256=pWZqbZP9-7ChHNK1DwfSlt9hU6lcJjvNXmvduR2G5tQ,33507
|
|
41
41
|
ops/inventory/caching.py,sha256=G2sJJ7nPHCqNBz3Vh0vxrFWQZt6B_11yS50GeZ8_1Bs,2000
|
|
42
|
-
ops/inventory/ec2inventory.py,sha256=
|
|
42
|
+
ops/inventory/ec2inventory.py,sha256=Zz3pB6hGLkzZa_ZIwpF2DfBXM_IQDIUmlwUIyy5D2HU,10655
|
|
43
43
|
ops/inventory/generator.py,sha256=BcjUxMoWTnmx-P_S-1fLjqG3PUhyMyAO2S-037KG5q0,10885
|
|
44
|
-
ops/inventory/sshconfig.py,sha256=
|
|
44
|
+
ops/inventory/sshconfig.py,sha256=lxqKyH0uCRCk4XXZISGrtX_d0c7OREg2Hh77ndtz05k,4438
|
|
45
45
|
ops/inventory/plugin/__init__.py,sha256=kSOYPdE9T0Ixe6tQUYoOl_udNbFZdVatvRf6bkf92aE,725
|
|
46
46
|
ops/inventory/plugin/azr.py,sha256=8-G3mEc0WQV7ZhJ4z6X-Wo90nQ7zAzHwgLpV4l_gbjo,5910
|
|
47
|
-
ops/inventory/plugin/cns.py,sha256=
|
|
48
|
-
ops/inventory/plugin/ec2.py,sha256=
|
|
47
|
+
ops/inventory/plugin/cns.py,sha256=X7CUMA8ltGxDfCkvaeEHsd60g8Y_C_gJk7287UM-qHw,1763
|
|
48
|
+
ops/inventory/plugin/ec2.py,sha256=M9Z_MJukDUyA1XtvHeiRGuTUh8A3ibHfDINJDUwLo-U,1332
|
|
49
49
|
ops/inventory/plugin/legacy_pcs.py,sha256=ii1kOkwD9PIaOlleJa3_yE95ojCoPlBPKSBu5naZhxw,1184
|
|
50
50
|
ops/inventory/plugin/skms.py,sha256=pFMpvfbXCf0KPgEOj5oAD7UzpUZ5zL1lcN4wESOjPuI,8517
|
|
51
51
|
ops/jinja/__init__.py,sha256=Nvmvb1edSAehNKYyroo26Jrxjzbu_TOCBS8Y9mMEOyA,1743
|
|
52
52
|
ops/terraform/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
53
|
-
ops/terraform/terraform_cmd_generator.py,sha256=
|
|
54
|
-
ops_cli-2.
|
|
55
|
-
ops_cli-2.
|
|
56
|
-
ops_cli-2.
|
|
57
|
-
ops_cli-2.
|
|
58
|
-
ops_cli-2.
|
|
59
|
-
ops_cli-2.
|
|
53
|
+
ops/terraform/terraform_cmd_generator.py,sha256=5yfzS7aOOjVDUYHwBadFvPjWKEFJWk9SEOhTZHywlG0,21728
|
|
54
|
+
ops_cli-2.3.0.dist-info/LICENSE,sha256=ff5lJoiLrFF1nJn5pRJiuicRqMEqBn8hgWCd2aQGa4Q,11335
|
|
55
|
+
ops_cli-2.3.0.dist-info/METADATA,sha256=fiCNonTcwfXYBj8vytZW3M66vBrCovowBpgYhv8AAXE,39904
|
|
56
|
+
ops_cli-2.3.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
57
|
+
ops_cli-2.3.0.dist-info/entry_points.txt,sha256=maaS2Tf8WvxMXckssedK13LXegD9jgHB2AT8xiEfVpQ,37
|
|
58
|
+
ops_cli-2.3.0.dist-info/top_level.txt,sha256=enC05wWafSg8iDKIvj3gvtAtEP2kYCyN5Gmd689q-_I,4
|
|
59
|
+
ops_cli-2.3.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|