Skip to content

Commit 509761f

Browse files
authored
Add new relic based container exec (#27)
1 parent de5f4c9 commit 509761f

18 files changed

+269
-148
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
__pycache__/
66
*.py[cod]
77
*$py.class
8+
.pytest_cache
89

910
# C extensions
1011
*.so

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ _upload: test
2020

2121
tag:
2222
git tag v`cat bridgy/version.py | grep __version__ | awk -F"=" '{print $$2}' | sed -e 's/^[ \t]*//' | tr -d "'"` && \
23-
git push --tags
23+
git push --tags
2424

2525
bootstrap3: venv3
2626
. venv3/bin/activate

bridgy/__main__.py

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
bridgy init
1010
bridgy ssh (-t | --tmux) [-adsuvw] [-l LAYOUT] <host>...
1111
bridgy ssh [-duv] <host>
12+
bridgy exec (-t | --tmux) [-adsuvw] [-l LAYOUT] <container>...
13+
bridgy exec [-duv] <container>
1214
bridgy list-inventory
1315
bridgy list-mounts
1416
bridgy mount [-duv] <host>:<remotedir>
@@ -21,6 +23,7 @@
2123
Sub-commands:
2224
init create the ~/.bridgy/config.yml
2325
ssh ssh into the selected host(s)
26+
exec exec into the selected container(s) (interactive + tty)
2427
mount use sshfs to mount a remote directory to an empty local directory
2528
unmount unmount one or more host sshfs mounts
2629
list-mounts show all sshfs mounts
@@ -41,13 +44,15 @@
4144
4245
Configuration Options are in ~/.bridgy/config.yml
4346
"""
44-
import sys
47+
import sys
4548
if sys.version_info < (3, 0):
4649
reload(sys)
4750
sys.setdefaultencoding('utf8')
4851

49-
import os
5052
import logging
53+
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
54+
55+
import os
5156
import inquirer
5257
from inquirer.themes import Theme
5358
import coloredlogs
@@ -57,6 +62,7 @@
5762

5863
from bridgy.version import __version__
5964
from bridgy.command import Ssh, Sshfs, RunAnsiblePlaybook
65+
from bridgy.inventory import InstanceType
6066
import bridgy.inventory as inventory
6167
import bridgy.config as cfg
6268
import bridgy.tmux as tmux
@@ -67,7 +73,7 @@
6773

6874

6975
class CustomTheme(Theme):
70-
76+
7177
def __init__(self):
7278
super(CustomTheme, self).__init__()
7379

@@ -82,19 +88,19 @@ def __init__(self):
8288
self.Checkbox.selected_icon = '◉ ' #✔⬢◉
8389
self.Checkbox.selected_color = selected_color
8490
self.Checkbox.unselected_color = utils.term.normal
85-
self.Checkbox.unselected_icon = '○ ' #▢ ○ ⬡ 🞅 ⭘ 🔿 🔾
91+
self.Checkbox.unselected_icon = '○ ' #▢ ○ ⬡ 🞅 ⭘ 🔿 🔾
8692
self.List.selection_color = selection_color
8793
self.List.selection_cursor = '‣' # ❯
8894
self.List.unselected_color = utils.term.normal
8995

9096
THEMER = CustomTheme()
9197

92-
def prompt_targets(question, targets=None, instances=None, multiple=True, config=None):
98+
def prompt_targets(question, targets=None, instances=None, multiple=True, config=None, type=InstanceType.ALL):
9399
if targets == None and instances == None or targets != None and instances != None:
94100
raise RuntimeError("Provide exactly one of either 'targets' or 'instances'")
95101

96102
if targets:
97-
instances = inventory.search(config, targets)
103+
instances = inventory.search(config, targets, type=type)
98104

99105
if len(instances) == 0:
100106
return []
@@ -143,17 +149,56 @@ def prompt_targets(question, targets=None, instances=None, multiple=True, config
143149
return selected_hosts
144150

145151

152+
@utils.SupportedPlatforms('linux', 'windows', 'osx')
153+
def exec_handler(args, config):
154+
if config.dig('inventory', 'update_at_start') or args['-u']:
155+
update_handler(args, config)
156+
157+
if args ['--tmux'] or config.dig('ssh', 'tmux'):
158+
question = "What containers would you like to exec into?"
159+
targets = prompt_targets(question, targets=args['<container>'], config=config, type=InstanceType.CONTAINER)
160+
else:
161+
question = "What containers would you like to exec into?"
162+
targets = prompt_targets(question, targets=args['<container>'], config=config, type=InstanceType.CONTAINER, multiple=False)
163+
164+
if len(targets) == 0:
165+
logger.info("No matching instances found")
166+
sys.exit(1)
167+
168+
for instance in targets:
169+
if instance.container_id == None:
170+
logger.info("Could not find container id for instance: %s" % instance)
171+
sys.exit(1)
172+
173+
commands = collections.OrderedDict()
174+
for idx, instance in enumerate(targets):
175+
name = '{}-{}'.format(instance.name, idx)
176+
commands[name] = Ssh(config, instance, command="sudo -i docker exec -ti %s bash" % instance.container_id).command
177+
178+
layout = None
179+
if args['--layout']:
180+
layout = args['--layout']
181+
182+
if args['--tmux'] or config.dig('ssh', 'tmux'):
183+
tmux.run(config, commands, args['-w'], layout, args['-d'], args['-s'])
184+
else:
185+
cmd = list(commands.values())[0]
186+
if args['-d']:
187+
logger.debug(cmd)
188+
else:
189+
os.system(cmd)
190+
146191
@utils.SupportedPlatforms('linux', 'windows', 'osx')
147192
def ssh_handler(args, config):
148193
if config.dig('inventory', 'update_at_start') or args['-u']:
149194
update_handler(args, config)
150195

151196
if args ['--tmux'] or config.dig('ssh', 'tmux'):
152197
question = "What instances would you like to ssh into?"
153-
targets = prompt_targets(question, targets=args['<host>'], config=config)
198+
targets = prompt_targets(question, targets=args['<host>'], config=config, type=InstanceType.VM)
154199
else:
155200
question = "What instance would you like to ssh into?"
156-
targets = prompt_targets(question, targets=args['<host>'], config=config, multiple=False)
201+
targets = prompt_targets(question, targets=args['<host>'], config=config, type=InstanceType.VM, multiple=False)
157202

158203
if len(targets) == 0:
159204
logger.info("No matching instances found")
@@ -257,12 +302,12 @@ def unmount_handler(args, config):
257302
@utils.SupportedPlatforms('linux', 'windows', 'osx')
258303
def list_inventory_handler(args, config):
259304
instances = []
260-
for ip, name, aliases, source in inventory.instances(config):
261-
if aliases:
262-
instances.append( (ip, name, '\n'.join(aliases), source) )
305+
for instance in sorted(inventory.instances(config)):
306+
if instance.aliases:
307+
instances.append( (instance.name, instance.address, '\n'.join(instance.aliases), instance.source, instance.type) )
263308
else:
264-
instances.append( (ip, name, '--- None ---', source) )
265-
logger.info(tabulate(instances, headers=['Name', 'Address/Dns', 'Aliases', 'Source']))
309+
instances.append( (instance.name, instance.address, '--- None ---', instance.source, instance.type) )
310+
logger.info(tabulate(instances, headers=['Name', 'Address/Dns', 'Aliases', 'Source', 'Type']))
266311

267312

268313
@utils.SupportedPlatforms('linux', 'windows', 'osx')
@@ -328,6 +373,7 @@ def main():
328373

329374
opts = {
330375
'ssh': ssh_handler,
376+
'exec': exec_handler,
331377
'mount': mount_handler,
332378
'list-mounts': list_mounts_handler,
333379
'list-inventory': list_inventory_handler,

bridgy/command/ssh.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33

44
class Ssh(object):
55

6-
def __init__(self, config, instance):
6+
def __init__(self, config, instance, command=''):
77
if not hasattr(config, '__getitem__'):
88
raise BadConfigError
99
if not isinstance(instance, tuple):
1010
raise BadInstanceError
1111

1212
self.config = config
1313
self.instance = instance
14+
self.custom_command = command
1415

1516
@property
1617
def destination(self):
@@ -34,11 +35,12 @@ def options(self):
3435

3536
options = self.config.dig('ssh', 'options') or ''
3637

37-
return '{} {}'.format(bastion, options)
38+
return '{} {} -t'.format(bastion, options)
3839

3940

4041
@property
4142
def command(self):
42-
cmd = 'ssh {options} {destination}'
43+
cmd = 'ssh {options} {destination} {command}'
4344
return cmd.format(destination=self.destination,
44-
options=self.options )
45+
options=self.options,
46+
command=self.custom_command)

bridgy/inventory/__init__.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from bridgy.utils import memoize
88
from bridgy.error import MissingBastionHost
9-
from bridgy.inventory.source import Bastion, Instance, InventorySet
9+
from bridgy.inventory.source import Bastion, Instance, InventorySet, InstanceType
1010
from bridgy.inventory.aws import AwsInventory
1111
from bridgy.inventory.flatfile import CsvInventory
1212
from bridgy.inventory.newrelic import NewRelicInventory
@@ -28,13 +28,13 @@ def inventory(config):
2828

2929
for source, srcCfg in config.sources():
3030
if source == 'aws':
31-
# the cache directory for the original v1 config did not separate
31+
# the cache directory for the original v1 config did not separate
3232
# out multiple aws profiles into subdirectories
3333
if config.version == 1:
3434
cache_dir = config.inventoryDir(AwsInventory.name)
3535
else:
3636
cache_dir = config.inventoryDir(AwsInventory.name, srcCfg['name'])
37-
37+
3838
if not os.path.exists(cache_dir):
3939
os.mkdir(cache_dir)
4040

@@ -71,9 +71,9 @@ def inventory(config):
7171
inv = NewRelicInventory(data_path=config.inventoryDir(NewRelicInventory.name),
7272
proxies=proxies,
7373
**srcCfg)
74-
74+
7575
inventorySet.add(inv)
76-
76+
7777
return inventorySet
7878

7979
def instance_filter(instance, include_re=None, exclude_re=None):
@@ -110,7 +110,7 @@ def instances(config):
110110
@memoize
111111
def get_bastion(config, instance):
112112
bastion = None
113-
113+
114114
for inv in inventory(config).inventories:
115115
if inv.name == instance.source and inv.bastion != None:
116116
bastion = inv.bastion
@@ -132,7 +132,7 @@ def get_bastion(config, instance):
132132

133133
return bastion
134134

135-
def search(config, targets):
135+
def search(config, targets, type=InstanceType.ALL):
136136
fuzzy = False
137137
if config.dig('inventory', 'fuzzy_search'):
138138
fuzzy = config.dig('inventory', 'fuzzy_search')
@@ -145,7 +145,11 @@ def search(config, targets):
145145

146146
matched_instances = inventory(config).search(targets, fuzzy=fuzzy)
147147
config_instance_filter = partial(instance_filter, include_re=include_re, exclude_re=exclude_re)
148-
return list(filter(config_instance_filter, matched_instances))
148+
filtered_instances = list(filter(config_instance_filter, matched_instances))
149+
if type == InstanceType.ALL:
150+
return filtered_instances
151+
else:
152+
return [x for x in filtered_instances if x.type == type]
149153

150154
def update(config):
151155
inventory(config).update()

bridgy/inventory/aws.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import placebo
44
import logging
55

6-
from bridgy.inventory.source import InventorySource, Instance
6+
from bridgy.inventory.source import InventorySource, Instance, InstanceType
77

88
logger = logging.getLogger()
99

@@ -82,9 +82,9 @@ def instances(self):
8282
# take note of this instance
8383
if name != None and address != None:
8484
if len(aliases) > 0:
85-
instances.append(Instance(name, address, tuple(aliases), self.name))
85+
instances.append(Instance(name, address, tuple(aliases), self.name, None, InstanceType.VM))
8686
else:
87-
instances.append(Instance(name, address, self.name))
87+
instances.append(Instance(name, address, None, self.name, None, InstanceType.VM))
8888

8989
return instances
9090

bridgy/inventory/flatfile.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import sys
44
import logging
55

6-
from bridgy.inventory.source import InventorySource, Instance
6+
from bridgy.inventory.source import InventorySource, Instance, InstanceType
77

88
logger = logging.getLogger()
99

@@ -29,7 +29,7 @@ def instances(self):
2929
with open(self.csv_path, 'r') as csv_file:
3030
reader = csv.DictReader(csv_file, fieldnames=self.fields, delimiter=self.delimiter)
3131
for row in reader:
32-
instances.add(Instance(row['name'].strip(), row['address'].strip(), None, self.name))
32+
instances.add(Instance(row['name'].strip(), row['address'].strip(), None, self.name, None, InstanceType.VM))
3333
except IOError as ex:
3434
logger.error("Unable to read inventory: %s" % ex)
3535
sys.exit(1)

bridgy/inventory/newrelic.py

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
except ImportError:
77
from urllib import quote_plus
88

9-
from bridgy.inventory.source import InventorySource, Instance
9+
from bridgy.inventory.source import InventorySource, Instance, InstanceType
10+
from bridgy.utils import parseIpFromHostname
1011

1112
class NewRelicInventory(InventorySource):
1213

@@ -22,7 +23,8 @@ def __init__(self, account_number, insights_query_api_key, data_path, proxies=No
2223
self.account_number = account_number
2324
self.insights_query_api_key = insights_query_api_key
2425
self.data_file = os.path.join(data_path, '%s.json' % str(account_number))
25-
self.query = quote_plus("SELECT entityName, fullHostname, hostname, ipV4Address from NetworkSample LIMIT 999")
26+
self.queryVms = quote_plus("SELECT entityName, fullHostname, hostname, ipV4Address from NetworkSample LIMIT 999")
27+
self.queryContainers = quote_plus("SELECT containerName, containerId, hostname FROM ProcessSample WHERE containerName IS NOT NULL LIMIT 999")
2628
if proxies:
2729
self.proxies = proxies
2830
else:
@@ -32,24 +34,42 @@ def update(self):
3234
headers = {'X-Query-Key': self.insights_query_api_key,
3335
'Accept': 'application/json'}
3436

35-
response = requests.get(NewRelicInventory.url.format(self.account_number, self.query),
37+
responseVms = requests.get(NewRelicInventory.url.format(self.account_number, self.queryVms),
38+
headers=headers,
39+
proxies=self.proxies)
40+
41+
responseContainers = requests.get(NewRelicInventory.url.format(self.account_number, self.queryContainers),
3642
headers=headers,
3743
proxies=self.proxies)
3844

3945
with open(self.data_file, 'w') as data_file:
40-
data_file.write(response.text)
46+
data_file.write(
47+
json.dumps({
48+
InstanceType.VM: json.loads(responseVms.text),
49+
InstanceType.CONTAINER: json.loads(responseContainers.text)
50+
})
51+
)
4152

4253
def instances(self):
4354
instances = set()
4455
with open(self.data_file, 'r') as data_file:
4556
data = json.load(data_file)
4657

47-
for results_dict in data['results']:
58+
for results_dict in data[InstanceType.VM]['results']:
4859
for event_dict in results_dict['events']:
4960
hostname = event_dict['hostname']
5061
address = event_dict['ipV4Address'].strip().split("/")[0]
51-
if hostname == None:
62+
if hostname is None:
5263
hostname = address
53-
instances.add(Instance(hostname, address, None, self.name))
64+
instances.add(Instance(hostname, address, None, self.name, None, InstanceType.VM))
65+
66+
for results_dict in data[InstanceType.CONTAINER]['results']:
67+
for event_dict in results_dict['events']:
68+
container_name = event_dict['containerName']
69+
container_id = event_dict['containerId']
70+
hostname = event_dict['hostname']
71+
address = parseIpFromHostname(hostname)
72+
73+
instances.add(Instance(container_name, address, None, self.name, container_id, InstanceType.CONTAINER))
5474

5575
return list(instances)

0 commit comments

Comments
 (0)