1module Control.Distributed.Process.Backend.Azure
5 , defaultAzureParameters
7 -- * Re-exports from Azure Service Management
13 -- * Remote and local processes
23import System.Environment (getEnv)
24import System.FilePath ((</>), takeFileName)
25import System.Environment.Executable (getExecutablePath)
26import System.IO (stdout, hFlush)
27import Data.Binary (Binary(get, put), encode, decode)
28import Data.Digest.Pure.MD5 (md5, MD5Digest)
29import qualified Data.ByteString as BSS
35import qualified Data.ByteString.Char8 as BSSC (pack)
36import qualified Data.ByteString.Lazy as BSL
44import qualified Data.ByteString.Lazy.Char8 as BSLC (unpack)
45import Data.Typeable (Typeable)
46import Control.Applicative ((<$>), (<*>))
47import Control.Monad (void, unless)
48import Control.Monad.Reader (MonadReader, ReaderT, runReaderT, ask)
49import Control.Exception (Exception, catches, Handler(Handler), throwIO)
50import Control.Monad.IO.Class (MonadIO, liftIO)
53import Network.Azure.ServiceManagement
59import qualified Network.Azure.ServiceManagement as Azure
66import qualified Network.SSH.Client.LibSSH2 as SSH
75import qualified Network.SSH.Client.LibSSH2.Foreign as SSH
82import qualified Network.SSH.Client.LibSSH2.Errors as SSH
89import Control.Distributed.Process (Process, Closure)
90import Control.Distributed.Process.Serializable (Serializable)
91import Control.Distributed.Process.Internal.Types
95import Network.Transport.Internal (encodeInt32, decodeInt32, prependLength)
98data Backend = Backend {
99 -- | Find virtual machines
100 findVMs :: IO [VirtualMachine]
101 -- | Copy the executable to a virtual machine
102 , copyToVM :: VirtualMachine -> IO ()
103 -- | Check the MD5 hash of the remote executable
104 , checkMD5 :: VirtualMachine -> IO Bool
105 -- | @runOnVM vm port pp@ starts a new CH node on machine @vm@ and then
106 -- runs the specified process pair. The CH node will shut down when the
107 -- /local/ process exists. @callOnVM@ returns the returned by the local
109 , callOnVM :: forall a. VirtualMachine -> String -> ProcessPair a -> IO a
110 -- | Create a new CH node and run the specified process.
111 -- The CH node will shut down when the /remote/ process exists. @spawnOnVM@
112 -- returns as soon as the process has been spawned.
113 , spawnOnVM :: VirtualMachine -> String -> RemoteProcess () -> IO ()
114 } deriving (Typeable)
116-- | Azure connection parameters
117data AzureParameters = AzureParameters {
118 azureSetup :: AzureSetup
119 , azureSshUserName :: FilePath
120 , azureSshPublicKey :: FilePath
121 , azureSshPrivateKey :: FilePath
122 , azureSshPassphrase :: String
123 , azureSshKnownHosts :: FilePath
124 , azureSshRemotePath :: FilePath
125 , azureSshLocalPath :: FilePath
128instance Binary AzureParameters where
130 put (azureSetup params)
131 put (azureSshUserName params)
132 put (azureSshPublicKey params)
133 put (azureSshPrivateKey params)
134 put (azureSshPassphrase params)
135 put (azureSshKnownHosts params)
136 put (azureSshRemotePath params)
137 put (azureSshLocalPath params)
139 AzureParameters <$> get <*> get <*> get <*> get <*> get <*> get <*> get <*> get
141-- | Create default azure parameters
142defaultAzureParameters :: String -- ^ Azure subscription ID
143 -> FilePath -- ^ Path to X509 certificate
144 -> FilePath -- ^ Path to private key
145 -> IO AzureParameters
146defaultAzureParameters sid x509 pkey = do
147 home <- getEnv "HOME"
148 user <- getEnv "USER"
149 self <- getExecutablePath
150 setup <- Azure.azureSetup sid x509 pkey
151 return AzureParameters
153 , azureSshUserName = user
154 , azureSshPublicKey = home </> ".ssh" </> "id_rsa.pub"
155 , azureSshPrivateKey = home </> ".ssh" </> "id_rsa"
156 , azureSshPassphrase = ""
157 , azureSshKnownHosts = home </> ".ssh" </> "known_hosts"
158 , azureSshRemotePath = takeFileName self
159 , azureSshLocalPath = self
162-- | Initialize the backend
163initializeBackend :: AzureParameters -- ^ Connection parameters
164 -> String -- ^ Cloud service name
166initializeBackend params cloudService =
168 findVMs = apiFindVMs params cloudService
169 , copyToVM = apiCopyToVM params
170 , checkMD5 = apiCheckMD5 params
171 , callOnVM = apiCallOnVM params cloudService
172 , spawnOnVM = apiSpawnOnVM params cloudService
175-- | Find virtual machines
176apiFindVMs :: AzureParameters -> String -> IO [VirtualMachine]
177apiFindVMs params cloudService = do
178 css <- Azure.cloudServices (azureSetup params)
179 case filter ((== cloudService) . cloudServiceName) css of
180 [cs] -> return $ cloudServiceVMs cs
183-- | Start a CH node on the given virtual machine
184apiCopyToVM :: AzureParameters -> VirtualMachine -> IO ()
185apiCopyToVM params vm =
186 void . withSSH2 params vm $ \s -> catchSshError s $
187 SSH.scpSendFile s 0o700 (azureSshLocalPath params) (azureSshRemotePath params)
189-- | Call a process on a VM
190apiCallOnVM :: AzureParameters
196apiCallOnVM params cloudService vm port ppair =
197 withSSH2 params vm $ \s -> do
198 let exe = "PATH=. " ++ azureSshRemotePath params
200 ++ " --host " ++ vmIpAddress vm
201 ++ " --port " ++ port
202 ++ " --cloud-service " ++ cloudService
204 let paramsEnc = encode params
205 (status, r) <- SSH.withChannelBy (SSH.openChannelSession s) id $ \ch -> do
206 SSH.channelExecute ch exe
207 SSH.writeChannel ch (encodeInt32 (BSL.length rprocEnc))
208 SSH.writeAllChannel ch rprocEnc
209 SSH.writeChannel ch (encodeInt32 (BSL.length paramsEnc))
210 SSH.writeAllChannel ch paramsEnc
211 runLocalProcess (ppairLocal ppair) ch
214 else error "callOnVM: Non-zero exit status"
216 rprocEnc :: BSL.ByteString
217 rprocEnc = encode (ppairRemote ppair)
219apiSpawnOnVM :: AzureParameters
223 -> Closure (Backend -> Process ())
225apiSpawnOnVM params cloudService vm port proc =
226 withSSH2 params vm $ \s -> do
227 -- TODO: reduce duplication with apiCallOnVM
228 let exe = "PATH=. " ++ azureSshRemotePath params
230 ++ " --host " ++ vmIpAddress vm
231 ++ " --port " ++ port
232 ++ " --cloud-service " ++ cloudService
235 let paramsEnc = encode params
236 (status, r) <- SSH.withChannelBy (SSH.openChannelSession s) id $ \ch -> do
237 SSH.channelExecute ch exe
238 SSH.writeChannel ch (encodeInt32 (BSL.length procEnc))
239 SSH.writeAllChannel ch procEnc
240 SSH.writeChannel ch (encodeInt32 (BSL.length paramsEnc))
241 SSH.writeAllChannel ch paramsEnc
242 SSH.channelSendEOF ch
243 SSH.readAllChannel ch
244 unless (status == 0) $ error (BSLC.unpack r)
246 procEnc :: BSL.ByteString
247 procEnc = encode proc
249-- | Check the MD5 hash of the executable on the remote machine
250apiCheckMD5 :: AzureParameters -> VirtualMachine -> IO Bool
251apiCheckMD5 params vm = do
252 hash <- localHash params
253 withSSH2 params vm $ \s -> do
254 (r, _) <- SSH.withChannelBy (SSH.openChannelSession s) id $ \ch -> do
255 SSH.channelExecute ch "md5sum -c --status"
256 SSH.writeChannel ch . BSSC.pack $ show hash ++ " " ++ azureSshRemotePath params
257 SSH.channelSendEOF ch
258 SSH.readAllChannel ch
261withSSH2 :: AzureParameters -> VirtualMachine -> (SSH.Session -> IO a) -> IO a
262withSSH2 params (Azure.vmSshEndpoint -> Just ep) =
263 SSH.withSSH2 (azureSshKnownHosts params)
264 (azureSshPublicKey params)
265 (azureSshPrivateKey params)
266 (azureSshPassphrase params)
267 (azureSshUserName params)
269 (read $ endpointPort ep)
271 error $ "withSSH2: No SSH endpoint for virtual machine " ++ vmName vm
273catchSshError :: SSH.Session -> IO a -> IO a
275 catches io [ Handler handleErrorCode
276 , Handler handleNullPointer
279 handleErrorCode :: SSH.ErrorCode -> IO a
280 handleErrorCode _ = do
281 (_, str) <- SSH.getLastError s
284 handleNullPointer :: SSH.NULL_POINTER -> IO a
285 handleNullPointer _ = do
286 (_, str) <- SSH.getLastError s
289localHash :: AzureParameters -> IO MD5Digest
290localHash params = md5 <$> BSL.readFile (azureSshLocalPath params)
292--------------------------------------------------------------------------------
293-- Local and remote processes --
294--------------------------------------------------------------------------------
296-- | A process pair consists of a remote process and a local process. The local
297-- process can send messages to the remote process using 'localSend' and wait
298-- for messages from the remote process using 'localExpect'. The remote process
299-- can send messages to the local process using 'remoteSend', and wait for
300-- messages from the local process using the standard Cloud Haskell primitives.
302-- See also 'callOnVM'.
303data ProcessPair a = ProcessPair {
304 ppairRemote :: RemoteProcess ()
305 , ppairLocal :: LocalProcess a
308-- | The process to run on the remote node (see 'ProcessPair' and 'callOnVM').
309type RemoteProcess a = Closure (Backend -> Process a)
311-- | The process to run on the local node (see 'ProcessPair' and 'callOnVM').
312newtype LocalProcess a = LocalProcess { unLocalProcess :: ReaderT SSH.Channel IO a }
313 deriving (Functor, Monad, MonadIO, MonadReader SSH.Channel)
315runLocalProcess :: LocalProcess a -> SSH.Channel -> IO a
316runLocalProcess = runReaderT . unLocalProcess
318-- | Send a messages from the local process to the remote process
319-- (see 'ProcessPair')
320localSend :: Serializable a => a -> LocalProcess ()
321localSend x = LocalProcess $ do
323 liftIO $ mapM_ (SSH.writeChannel ch)
329-- | Wait for a message from the remote process (see 'ProcessPair').
330-- Note that unlike for the standard Cloud Haskell 'expect' it will result in a
331-- runtime error if the remote process sends a message of type other than @a@.
332localExpect :: Serializable a => LocalProcess a
333localExpect = LocalProcess $ do
336 isE <- readIntChannel ch
337 len <- readIntChannel ch
338 msg <- readSizeChannel ch len
340 then error (decode msg)
341 else return (decode msg)
343-- | Send a message from the remote process to the local process (see
344-- 'ProcessPair'). Note that the remote process can use the standard Cloud
345-- Haskell primitives to /receive/ messages from the local process.
346remoteSend :: Serializable a => a -> Process ()
347remoteSend = liftIO . remoteSend' 0
349-- | If the remote process encounters an error it can use 'remoteThrow'. This
350-- will cause the exception to be raised (as a user-exception, not as the
351-- original type) in the local process (as well as in the remote process).
352remoteThrow :: Exception e => e -> IO ()
353remoteThrow e = remoteSend' 1 (show e) >> throwIO e
355remoteSend' :: Serializable a => Int -> a -> IO ()
356remoteSend' flags x = do
358 BSS.hPut stdout (encodeInt32 flags)
359 BSS.hPut stdout (encodeInt32 (BSL.length enc))
363--------------------------------------------------------------------------------
365--------------------------------------------------------------------------------
367readSizeChannel :: SSH.Channel -> Int -> IO BSL.ByteString
368readSizeChannel ch = go []
370 go :: [BSS.ByteString] -> Int -> IO BSL.ByteString
371 go acc 0 = return (BSL.fromChunks $ reverse acc)
373 bs <- SSH.readChannel ch (fromIntegral (0x400 `min` size))
374 go (bs : acc) (size - BSS.length bs)
376readIntChannel :: SSH.Channel -> IO Int
378 decodeInt32 . BSS.concat . BSL.toChunks <$> readSizeChannel ch 4
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list